Files
docker-podman-installation-…/scripts/lib/common.sh
T
duffyduckandClaude Opus 5 2d645bcc0b Fehler in der Oberflaeche abfangen, Backups planbar machen
Der Anlass: "Backup erstellen" bei fehlendem Container beendete die
ganze TUI mit einer Fehlerzeile in der Shell.

Ursachen waren zwei, beide behoben:
  1. die() in GUI-Pfaden. Neue Helfer gui_need_container/gui_need_sites
     melden das Problem im Dialog und kehren zurueck. Interaktives wie
     Shell und Terminal-Browser laeuft jetzt ueber gui_run_interactive
     in einer Subshell mit Weiter-mit-ENTER.
  2. "set -e". Schon ein Rueckgabewert != 0 aus einer Menuefunktion
     beendete das Skript, weil Menuezweige keine Bedingung sind. 47
     "|| return" auf "|| return 0" gestellt und 37 Menuezweige mit
     "|| true" abgesichert. Ausserdem schlug ein "clear" ohne TERM fehl
     und riss alles mit - drei Aufrufe abgesichert.
Jeder Hauptmenuepunkt wurde einzeln durchlaufen und kehrt sauber zurueck.

Neu: Backup-Jobs
  Der Menuepunkt heisst jetzt "Backup erstellen / planen" mit Untermenue
  (manuell erstellen, Jobs verwalten, Automatik ein/aus, Zeitplan
  anzeigen, Job sofort ausfuehren). Dieselben Jobs liegen auch in den
  Einstellungen unter "Backup-Jobs".

  Manuelle Backups nehmen weiterhin immer BACKUP_DIR aus der .env.
  Jeder Job hat dagegen sein eigenes Zielverzeichnis, seinen eigenen
  Zeitplan, eigene Optionen und eine eigene Aufbewahrung, nach der er
  sein Zielverzeichnis selbst aufraeumt.

  Vollstaendig auch per Parameter:
    job list | status | add | set | del | enable | disable
        | run | run-all | auto [on|off]
  mit --name --sites --cron --dir --keep --no-rrds --no-logs --stop
  --disabled.

  "job auto on" erzeugt aus den aktiven Jobs /etc/cron.d/checkmk-backup,
  eine Zeile je Job mit dessen eigenem Zeitplan. Ohne root werden die
  Zeilen ausgegeben statt kommentarlos zu scheitern. Der Schalter steht
  als BACKUP_AUTO in der .env, die Jobs in backup-jobs.conf.

Nebenbei behoben: "local f; f=... tmp" fuehrte tmp als Kommando aus,
wodurch job_put und job_remove nichts schrieben.

Dokumentiert als Abschnitt 15.1 in beiden Sprachfassungen.

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

340 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_ENABLED="${NPM_ENABLED:-on}"
BACKUP_AUTO="${BACKUP_AUTO:-off}"
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:-?}"
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
}