Files
docker-podman-installation-…/scripts/lib/common.sh
T
duffyduckandClaude Opus 5 9417f0c102 Dialoge passen sich an Terminal, Text und Anzahl der Eintraege an
Die Fenstergroesse war fest (22x78, Liste 13 Zeilen). Bei einem langen
Erklaertext blieb davon nur eine sichtbare Listenzeile uebrig - die
Auswahl liess sich nur noch scrollend bedienen.

ui_calc berechnet die Groesse jetzt aus Terminalgroesse, Zeilenzahl des
Textes und Anzahl sowie Breite der Eintraege; ui_calc_text macht
dasselbe fuer Meldung, Abfrage und Eingabe, ui_text nutzt fast das
ganze Terminal. Damit sind beim beanstandeten Dialog alle drei
Eintraege sichtbar statt einem, und in einem 45-Zeilen-Terminal passen
alle 13 Hauptmenue-Punkte gleichzeitig hin.

Beim Bauen gleich wieder in die bekannte Falle getappt und behoben:
"[ bedingung ] && VAR=wert" liefert 1, wenn die Bedingung nicht
zutrifft, und beendet unter "set -e" die Funktion. Die Berechnung
verwendet deshalb durchgehend if-Bloecke; das ist im Quelltext
vermerkt, damit es nicht wieder passiert.

Geprueft: Geometrie fuer den beanstandeten Dialog und fuer lange
Listen, Rendern aller Menues unter whiptail in einem echten
Pseudo-Terminal ohne Fehlermeldung, und alle Menues kehren weiterhin
sauber zurueck.

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

425 lines
15 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_ENABLED="${NPM_ENABLED:-on}"
BACKUP_AUTO="${BACKUP_AUTO:-off}"
PROXY_FORWARD_HOST="${PROXY_FORWARD_HOST:-127.0.0.1}"
PROXY_FORWARD_PORT="${PROXY_FORWARD_PORT:-5000}"
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
# ---------------------------------------------------------------------
# Ist der NGINX Proxy Manager eingeschaltet? (on/true/1/yes = ja)
npm_enabled() {
case "$(printf '%s' "${NPM_ENABLED:-on}" | tr 'A-Z' 'a-z')" in
on|true|1|yes|ja|y) return 0 ;;
*) return 1 ;;
esac
}
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:-?}"
# Groesse des Terminals ermitteln (mit Rueckfallwerten)
ui_term_size() {
UI_ROWS="$(tput lines 2>/dev/null || true)"
UI_COLS="$(tput cols 2>/dev/null || true)"
case "$UI_ROWS" in ''|*[!0-9]*) UI_ROWS=24 ;; esac
case "$UI_COLS" in ''|*[!0-9]*) UI_COLS=80 ;; esac
if [ "$UI_ROWS" -lt 10 ]; then UI_ROWS=24; fi
if [ "$UI_COLS" -lt 40 ]; then UI_COLS=80; fi
return 0
}
# Fenstergroesse aus Textlaenge, Anzahl der Eintraege und Terminalgroesse
# berechnen. Vorher waren die Werte fest - bei langem Text blieb dann nur
# eine Zeile fuer die Liste uebrig.
#
# ACHTUNG: hier bewusst mit if-Bloecken statt "[ ... ] && VAR=...".
# Letzteres liefert 1, wenn die Bedingung nicht zutrifft, und beendet
# unter "set -e" die Funktion bzw. das ganze Skript.
#
# ui_calc TEXT FELDER_JE_EINTRAG EINTRAEGE...
ui_calc() {
local text="$1" per="$2"; shift 2
ui_term_size
local tl maxw
tl="$(printf '%s\n' "$text" | wc -l)"
maxw="$(printf '%s\n' "$text" | awk '{ if (length($0) > m) m = length($0) } END { print m+0 }')"
# breiteste Zeile der Liste (Kennung und Beschriftung stehen nebeneinander)
local -a args=("$@")
local i=0 w itemw=0 n=0
while [ "$i" -lt "${#args[@]}" ]; do
w=$(( ${#args[$i]} + 4 ))
if [ $((i+1)) -lt "${#args[@]}" ]; then
w=$(( w + ${#args[$((i+1))]} ))
fi
if [ "$w" -gt "$itemw" ]; then itemw="$w"; fi
i=$(( i + per )); n=$(( n + 1 ))
done
# Breite: der breitere von Text und Liste, plus Rahmen
if [ "$maxw" -gt "$itemw" ]; then UI_W=$(( maxw + 10 )); else UI_W=$(( itemw + 10 )); fi
if [ "$UI_W" -lt 60 ]; then UI_W=60; fi
if [ "$UI_W" -gt $(( UI_COLS - 4 )) ]; then UI_W=$(( UI_COLS - 4 )); fi
# Listenhoehe: moeglichst alle Eintraege zeigen, ohne aus dem Bild zu laufen
local avail=$(( UI_ROWS - tl - 9 ))
if [ "$avail" -lt 3 ]; then avail=3; fi
if [ "$n" -lt "$avail" ]; then UI_MH="$n"; else UI_MH="$avail"; fi
if [ "$UI_MH" -lt 1 ]; then UI_MH=1; fi
UI_H=$(( tl + UI_MH + 8 ))
if [ "$UI_H" -gt $(( UI_ROWS - 2 )) ]; then UI_H=$(( UI_ROWS - 2 )); fi
if [ "$UI_H" -lt 10 ]; then UI_H=10; fi
return 0
}
# Hoehe fuer Fenster ohne Liste (Meldung, Abfrage, Eingabe)
ui_calc_text() { # text zusatzzeilen
local text="$1" extra="${2:-7}"
ui_term_size
local tl maxw
tl="$(printf '%s\n' "$text" | wc -l)"
maxw="$(printf '%s\n' "$text" | awk '{ if (length($0) > m) m = length($0) } END { print m+0 }')"
UI_W=$(( maxw + 10 ))
if [ "$UI_W" -lt 60 ]; then UI_W=60; fi
if [ "$UI_W" -gt $(( UI_COLS - 4 )) ]; then UI_W=$(( UI_COLS - 4 )); fi
UI_H=$(( tl + extra ))
if [ "$UI_H" -gt $(( UI_ROWS - 2 )) ]; then UI_H=$(( UI_ROWS - 2 )); fi
if [ "$UI_H" -lt 8 ]; then UI_H=8; fi
return 0
}
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_calc "$text" 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_calc "$text" 3 "$@"
"$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_calc "$text" 3 "$@"
"$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_calc_text "$2" 10
"$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" \
--inputbox "$2" "$UI_H" "$UI_W" "${3:-}" 3>&1 1>&2 2>&3
}
ui_password() {
ui_init
ui_calc_text "$2" 10
"$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" \
--passwordbox "$2" "$UI_H" "$UI_W" 3>&1 1>&2 2>&3
}
ui_msg() { # title text
ui_init
ui_calc_text "$2" 7
"$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --msgbox "$2" "$UI_H" "$UI_W"
}
ui_yesno() { # title text -> exit 0 = ja
ui_init
ui_calc_text "$2" 8
"$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --yesno "$2" "$UI_H" "$UI_W"
}
ui_text() { # title file - so gross wie das Terminal es zulaesst
ui_init
ui_term_size
"$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --textbox "$2" \
$(( UI_ROWS - 2 )) $(( UI_COLS - 4 ))
}
ui_info() { # title text (ohne OK-Button)
ui_init
ui_calc_text "$2" 5
"$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --infobox "$2" "$UI_H" "$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
}