#!/usr/bin/env bash
#
# Holt Agent, Hilfsprogramme und mitgelieferte Plugins vom Checkmk-Server.
#
# Grundsatz: NICHTS wird installiert, was nicht vorher geprueft wurde. Eine
# funktionierende Datei darf niemals durch eine kaputte ersetzt werden - lieber
# bleibt der alte Stand stehen und der Lauf meldet einen Fehler.
#
set -euo pipefail

CONF="${CONF:-/etc/cmk-push-agent/cmk-push-agent.conf}"
BIN_DIR="${BIN_DIR:-/usr/bin}"
PLUGIN_DIR="${PLUGIN_DIR:-/usr/lib/check_mk_agent/plugins}"

# Muss zum Intervall des Plugin-Managers passen. Liegt eine Kopie zusaetzlich
# direkt in PLUGIN_DIR, fuehrt der Agent das Plugin doppelt aus.
PLUGIN_INTERVAL="${PLUGIN_INTERVAL:-3600}"

PLUGIN_MANAGER="${PLUGIN_MANAGER:-/usr/bin/cmk-push-plugin-manager}"

# Konfigurationsverzeichnis des Agents. mk_mysql liest von dort zwingend eine
# mysql.cfg - anders als mk_postgres, das ohne Konfiguration auskommt.
MK_CONFDIR="${MK_CONFDIR:-/etc/check_mk}"
MYSQL_CFG="${MYSQL_CFG:-$MK_CONFDIR/mysql.cfg}"

# TLS-Pruefung. Der Default 1 (unsicher) entspricht dem bisherigen Verhalten
# mit selbstsigniertem Zertifikat auf dem Checkmk-Server.
CMK_INSECURE="${CMK_INSECURE:-1}"

have() { command -v "$1" >/dev/null 2>&1; }

log()  { printf '[%s] %s\n' "$(date -Is)" "$*"; }
warn() { printf '[%s] WARN: %s\n' "$(date -Is)" "$*" >&2; }
die()  { printf '[%s] ERROR: %s\n' "$(date -Is)" "$*" >&2; exit 2; }

FAILED=0
fail() { warn "$*"; FAILED=$(( FAILED + 1 )); }

[[ -f "$CONF" ]] || exit 0
# shellcheck disable=SC1090
source "$CONF"

: "${CMK_SERVER:?CMK_SERVER ist in $CONF nicht gesetzt}"
: "${CMK_SITE:?CMK_SITE ist in $CONF nicht gesetzt}"

BASE="https://${CMK_SERVER}/${CMK_SITE}/check_mk/agents"

# Eigenes Arbeitsverzeichnis statt /tmp: dort kann jeder Benutzer Dateien
# unterschieben oder Symlinks legen, in die wir dann hineinschreiben.
WORK="$(mktemp -d)" || die "mktemp fehlgeschlagen"
trap 'rm -rf "$WORK"' EXIT

CURL_OPTS=(-fsS --connect-timeout 10 --max-time 180 --retry 2 --retry-delay 3)
[[ "$CMK_INSECURE" == "1" ]] && CURL_OPTS+=(-k)

########################### Holen ##########################################

# -f laesst curl bei HTTP >= 400 fehlschlagen, statt die Fehlerseite in die
# Datei zu schreiben. Genau das war bisher der Weg, auf dem eine HTML-Seite
# als "Agent" in /usr/bin landen konnte.
fetch() {  # $1 = url, $2 = zieldatei
  local url="$1" out="$2" code
  code="$(curl "${CURL_OPTS[@]}" -w '%{http_code}' -o "$out" "$url" 2>/dev/null)" || {
    warn "Download fehlgeschlagen: $url (HTTP ${code:-?})"
    return 1
  }
  [[ -s "$out" ]] || { warn "Leere Datei geladen: $url"; return 1; }
  return 0
}

########################### Pruefen #########################################

min_size() {  # $1 = datei, $2 = mindestgroesse
  local sz; sz="$(stat -c %s "$1" 2>/dev/null || echo 0)"
  (( sz >= $2 )) || { warn "$1 ist mit ${sz} Byte zu klein (erwartet >= $2)"; return 1; }
  return 0
}

# Ein abgeschnittener Download faellt fast immer durch die Syntaxpruefung -
# das ist deutlich belastbarer als ein "head | grep Checkmk", das auch auf
# einer Fehlerseite anschlagen kann, die zufaellig das Wort enthaelt.
validate_shell() {  # $1 = datei
  local f="$1"
  min_size "$f" 512 || return 1
  head -c 2 -- "$f" | grep -q '#!' || { warn "$f hat keinen Shebang"; return 1; }
  bash -n -- "$f" 2>/dev/null || { warn "$f ist syntaktisch kaputt (abgeschnitten?)"; return 1; }
  grep -qiE 'check_?mk' -- "$f" || { warn "$f enthaelt keine Checkmk-Kennung"; return 1; }
  return 0
}

validate_python() {  # $1 = datei
  local f="$1"
  min_size "$f" 512 || return 1
  head -c 2 -- "$f" | grep -q '#!' || { warn "$f hat keinen Shebang"; return 1; }
  if command -v python3 >/dev/null 2>&1; then
    ( cd "$WORK" && python3 -m py_compile "$f" ) 2>/dev/null \
      || { warn "$f laesst sich nicht kompilieren (abgeschnitten?)"; return 1; }
  else
    warn "python3 fehlt - $f nur oberflaechlich geprueft"
  fi
  grep -qiE 'check_?mk|postgres' -- "$f" || { warn "$f wirkt inhaltlich falsch"; return 1; }
  return 0
}

# Bei einem Binary sagt die Syntaxpruefung nichts. ELF-Magic plus ein echter
# Probelauf faengt zusaetzlich den Fall ab, dass der Server ein Binary fuer
# eine andere Architektur ausliefert.
validate_elf() {  # $1 = datei, $2... = Testaufruf
  local f="$1"; shift
  min_size "$f" 1024 || return 1
  local magic; magic="$(head -c 4 -- "$f" | od -An -tx1 | tr -d ' \n')"
  [[ "$magic" == "7f454c46" ]] || { warn "$f ist keine ELF-Binary"; return 1; }
  if (( $# > 0 )); then
    chmod 0750 -- "$f"
    "$f" "$@" >/dev/null 2>&1 || { warn "$f laesst sich nicht ausfuehren (falsche Architektur?)"; return 1; }
  fi
  return 0
}

########################### Installieren ####################################

# Atomar: temporaere Datei IM ZIELVERZEICHNIS anlegen und per rename ersetzen.
# Zwei Vorteile gegenueber cp:
#   - es gibt keinen Moment, in dem die Zieldatei halb geschrieben ist
#   - rename ersetzt den Verzeichniseintrag, nicht den Inhalt der laufenden
#     Datei. Damit entfaellt "Text file busy" und das bisherige "kill -9" auf
#     laufende mk-job-Prozesse, das mitten laufende Cronjobs abgeschossen hat.
install_atomic() {  # $1 = quelle, $2 = ziel, $3 = mode
  local src="$1" dst="$2" mode="$3" tmp
  if [[ -e "$dst" ]] && cmp -s -- "$src" "$dst"; then
    log "unveraendert: $dst"
    return 0
  fi
  mkdir -p -- "$(dirname -- "$dst")"
  tmp="$(mktemp -- "${dst}.new.XXXXXX")" || { warn "mktemp in $(dirname -- "$dst") fehlgeschlagen"; return 1; }
  if ! cat -- "$src" > "$tmp"; then
    rm -f -- "$tmp"; warn "Schreiben nach $tmp fehlgeschlagen"; return 1
  fi
  chmod "$mode" -- "$tmp"
  mv -f -- "$tmp" "$dst" || { rm -f -- "$tmp"; warn "Ersetzen von $dst fehlgeschlagen"; return 1; }
  log "aktualisiert: $dst"
  return 0
}

# Entfernt Kopien desselben Plugins aus allen anderen verwalteten Orten -
# direkt in PLUGIN_DIR (der alte, ungecachte Platz) und in anderen
# Intervallverzeichnissen. Bleibt eine liegen, fuehrt der Agent das Plugin
# doppelt aus und die Section erscheint zweimal.
purge_other_copies() {  # $1 = name, $2 = behalten
  local name="$1" keep="$2" d p
  for p in "$PLUGIN_DIR/$name" "$PLUGIN_DIR"/[0-9]*/"$name"; do
    [[ -f "$p" && ! -L "$p" ]] || continue
    [[ "$p" == "$keep" ]] && continue
    rm -f -- "$p" && log "entfernt (doppelte Kopie): $p"
  done
  return 0
}

remove_plugin() {  # $1 = name - wenn die Voraussetzung nicht mehr gilt
  local name="$1" p removed=0
  for p in "$PLUGIN_DIR/$name" "$PLUGIN_DIR"/[0-9]*/"$name"; do
    [[ -f "$p" && ! -L "$p" ]] || continue
    rm -f -- "$p" && { log "entfernt (nicht mehr zutreffend): $p"; removed=1; }
  done
  (( removed )) && rm -f -- "/var/lib/check_mk_agent/cache/plugins_${name}.cache" 2>/dev/null
  return 0
}

# update_file <url> <ziel> <mode> <validator> [validator-args...]
update_file() {
  local url="$1" dst="$2" mode="$3" validator="$4"; shift 4
  local name tmp
  name="$(basename -- "$dst")"
  tmp="$WORK/$name"
  log "Hole $name"
  fetch "$url" "$tmp" || { fail "$name: Download fehlgeschlagen, alter Stand bleibt"; return 1; }
  "$validator" "$tmp" "$@" || { fail "$name: Pruefung fehlgeschlagen, alter Stand bleibt"; return 1; }
  install_atomic "$tmp" "$dst" "$mode" || { fail "$name: Installation fehlgeschlagen"; return 1; }
  return 0
}


########################### MySQL / MariaDB #################################

mysql_admin_bin() {
  local b
  for b in mariadb-admin mysqladmin; do have "$b" && { command -v "$b"; return 0; }; done
  return 1
}

# Bevorzugt der Socket, an dem der Server tatsaechlich lauscht - Konfigdateien
# und einkompilierte Defaults gehen bei abweichenden Setups daneben.
mysql_socket() {
  local s
  if have ss; then
    s="$(ss -lxn 2>/dev/null | grep -oE '/[^[:space:]]*mysqld?\.sock' | head -n1)"
    [[ -n "$s" ]] && { printf '%s\n' "$s"; return 0; }
  fi
  for s in /run/mysqld/mysqld.sock /var/run/mysqld/mysqld.sock \
           /var/lib/mysql/mysql.sock /tmp/mysql.sock; do
    [[ -S "$s" ]] && { printf '%s\n' "$s"; return 0; }
  done
  return 1
}

mysql_present() {
  mysql_admin_bin >/dev/null || return 1
  systemctl is-active mariadb.service mysql.service mysqld.service 2>/dev/null \
    | grep -q '^active$' && return 0
  mysql_socket >/dev/null && return 0
  return 1
}

# Marker in selbst erzeugten Dateien. Nur solche werden spaeter wieder
# angefasst - eine vom Administrator gepflegte mysql.cfg bleibt unberuehrt.
CFG_MARKER="# managed-by: cmk-agent-update"

mysql_cfg_is_ours() {
  [[ -f "$MYSQL_CFG" ]] && head -n 5 -- "$MYSQL_CFG" | grep -qF "$CFG_MARKER"
}

# Die Datei kann Zugangsdaten enthalten und darf niemandem ausser root
# zugaenglich sein. Eigene Dateien werden stillschweigend korrigiert, fremde
# nur gemeldet - dort koennte die Rechtevergabe Absicht sein (etwa ein
# Monitoring-Benutzer mit eigener Gruppe).
check_mysql_cfg_perms() {
  local mode owner
  [[ -f "$MYSQL_CFG" ]] || return 0
  mode="$(stat -c %a "$MYSQL_CFG" 2>/dev/null)"; mode="${mode: -3}"
  owner="$(stat -c %U "$MYSQL_CFG" 2>/dev/null)"

  if (( 8#${mode:-777} & 0077 )) || [[ "$owner" != "root" ]]; then
    if mysql_cfg_is_ours; then
      chmod 0400 -- "$MYSQL_CFG" 2>/dev/null && chown root:root -- "$MYSQL_CFG" 2>/dev/null
      log "Rechte korrigiert: $MYSQL_CFG war $mode $owner, jetzt 400 root"
    else
      warn "$MYSQL_CFG ist $mode und gehoert $owner - enthaelt moeglicherweise Zugangsdaten und ist fuer andere lesbar (chmod 0400, chown root:root)"
    fi
  fi
  return 0
}

# Panels installieren MySQL oft mit passwortgeschuetztem root. Die Zugangs-
# daten liegen dann aber auf dem System.
#
# WICHTIG: In MariaDB/MySQL sind 'root'@'localhost' und 'root'@'127.0.0.1'
# ZWEI VERSCHIEDENE Konten mit eigenen Passwoertern und Auth-Plugins. Ueber
# den Unix-Socket landet man immer bei 'localhost'. CloudPanel vergibt sein
# Master-Passwort an das TCP-Konto - deshalb gehoert der Host zur Zugangsdaten-
# Angabe dazu, sonst schlaegt die Anmeldung mit korrektem Passwort fehl.
#
# Ausgabe je Zeile: "quelle|user|passwort|host|port"   (host leer = Socket)
panel_db_creds_all() {
  local out u p h port f

  # --- CloudPanel: clpctl ---
  if have clpctl; then
    out="$(timeout 30 clpctl db:show:master-credentials 2>/dev/null)"
    cp_field() { sed -nE "s/.*\\|[[:space:]]*$1[[:space:]]*\\|[[:space:]]*(.*[^|[:space:]])[[:space:]]*\\|.*/\\1/p" <<< "$out" | head -n1; }
    u="$(cp_field 'User Name')"; p="$(cp_field 'Password')"
    h="$(cp_field 'Host')";      port="$(cp_field 'Port')"
    [[ -n "$u" && -n "$p" ]] && printf 'clpctl|%s|%s|%s|%s\n' "$u" "$p" "$h" "$port"
  fi

  # --- CloudPanel: Secrets-Datei ---
  if [[ -r /var/lib/cloudpanel/secrets/db_master_password ]]; then
    p="$(head -n1 -- /var/lib/cloudpanel/secrets/db_master_password | tr -d '\r\n')"
    [[ -n "$p" ]] && printf 'cloudpanel-secrets|root|%s|127.0.0.1|3306\n' "$p"
  fi

  # --- ISPConfig ---
  f=/usr/local/ispconfig/server/lib/mysql_clientdb.conf
  if [[ -r "$f" ]]; then
    u="$(sed -nE "s/^[[:space:]]*\\\$clientdb_user[[:space:]]*=[[:space:]]*'(.*)'[[:space:]]*;.*/\\1/p" "$f" | head -n1)"
    p="$(sed -nE "s/^[[:space:]]*\\\$clientdb_password[[:space:]]*=[[:space:]]*'(.*)'[[:space:]]*;.*/\\1/p" "$f" | head -n1)"
    h="$(sed -nE "s/^[[:space:]]*\\\$clientdb_host[[:space:]]*=[[:space:]]*'(.*)'[[:space:]]*;.*/\\1/p" "$f" | head -n1)"
    u="${u//\\\'/\'}"; p="${p//\\\'/\'}"
    [[ -n "$u" && -n "$p" ]] && printf 'ispconfig|%s|%s|%s|\n' "$u" "$p" "$h"
  fi

  # --- vorhandene Client-Konfigurationen ---
  for f in /root/.my.cnf /etc/mysql/debian.cnf /etc/mysql/root.cnf; do
    [[ -r "$f" ]] || continue
    u="$(sed -nE 's/^[[:space:]]*user[[:space:]]*=[[:space:]]*(.*)$/\1/p'     "$f" | head -n1)"
    p="$(sed -nE 's/^[[:space:]]*password[[:space:]]*=[[:space:]]*(.*)$/\1/p' "$f" | head -n1)"
    h="$(sed -nE 's/^[[:space:]]*host[[:space:]]*=[[:space:]]*(.*)$/\1/p'     "$f" | head -n1)"
    u="$(unquote "$u")"; p="$(unquote "$p")"; h="$(unquote "$h")"
    [[ -n "$u" && -n "$p" ]] && printf '%s|%s|%s|%s|\n' "${f##*/}" "$u" "$p" "$h"
  done
  return 0
}

# Werte in my.cnf: ein '#' nach Leerzeichen leitet einen Kommentar ein, und
# Panel-Passwoerter enthalten gern Sonderzeichen. Deshalb gequotet schreiben
# und Backslash sowie Anfuehrungszeichen escapen.
mycnf_quote() {
  local v="$1"
  v="${v//\\/\\\\}"
  v="${v//\"/\\\"}"
  printf '"%s"' "$v"
}

# Schreibt die Datei in einem von zwei Modi:
#   socket - nur Socket-Pfad, Anmeldung ueber unix_socket/auth_socket als root
#   creds  - zusaetzlich die vom Panel ausgelesenen Zugangsdaten
# Bevorzugt wird immer "socket": kein Passwort auf der Platte, nichts das
# ablaufen oder rotiert werden kann.
write_mysql_cfg() {  # $1 = socket|creds; bei creds: $2 user, $3 pass, $4 host, $5 port
  local mode="$1" sock tmp user pass
  sock="$(mysql_socket)" || { warn "MySQL-Socket nicht gefunden"; return 1; }
  # Verzeichnisrechte explizit setzen: bei offener umask (etwa aus einem
  # Cron-Kontext) wuerde mkdir sonst 0777 anlegen.
  mkdir -p -- "$MK_CONFDIR" || return 1
  chmod 0755 -- "$MK_CONFDIR" 2>/dev/null || true

  tmp="$(mktemp -- "${MYSQL_CFG}.new.XXXXXX")" || return 1
  # mktemp legt zwar 0600 an, aber die Rechte werden hier vor dem ersten
  # Schreibvorgang festgezurrt - danach steht das Passwort in der Datei.
  chmod 0600 -- "$tmp" || { rm -f -- "$tmp"; return 1; }
  chown root:root -- "$tmp" 2>/dev/null || true
  # Bricht das Skript zwischen Schreiben und Umbenennen ab, bliebe sonst eine
  # Datei mit Zugangsdaten liegen.
  trap 'rm -f -- "$tmp"' RETURN

  {
    printf '%s\n' "$CFG_MARKER"
    printf '#\n'
    printf '# Der Socket-Pfad muss hier stehen, weil mk_mysql die Clients mit\n'
    printf '# --defaults-file aufruft und damit alle anderen Konfigurationsdateien\n'
    printf '# ausgeblendet werden.\n'
    printf '[client]\n'
  } > "$tmp"

  if [[ "$mode" == "creds" ]]; then
    user="$2"; pass="$3"; host="${4:-}"; port="${5:-}"
    if [[ -z "$user" ]]; then
      rm -f -- "$tmp"; warn "keine Zugangsdaten uebergeben"; return 1
    fi
    {
      # "localhost" wuerde den Client wieder auf den Socket schicken und damit
      # auf ein anderes Konto - deshalb TCP nur mit expliziter Adresse.
      if [[ -n "$host" && "$host" != "localhost" ]]; then
        printf 'host=%s\n' "$host"
        printf 'protocol=tcp\n'
        [[ -n "$port" ]] && printf 'port=%s\n' "$port"
      else
        printf 'socket=%s\n' "$sock"
      fi
      printf 'user=%s\n' "$user"
      printf 'password=%s\n' "$(mycnf_quote "$pass")"
    } >> "$tmp"
  else
    printf 'socket=%s\n' "$sock" >> "$tmp"
    printf '# Ohne user/password: root authentifiziert sich per unix_socket.\n' >> "$tmp"
  fi

  chmod 0400 -- "$tmp"
  mv -f -- "$tmp" "$MYSQL_CFG" || { rm -f -- "$tmp"; return 1; }
  trap - RETURN
  log "geschrieben: $MYSQL_CFG (Modus $mode, Socket $sock)"
  return 0
}

# Stellt eine funktionierende mysql.cfg her. Reihenfolge:
#   1. vorhandene Datei mit einer echten Abfrage testen
#   2. socket-only versuchen (kein Passwort auf der Platte)
#   3. jede gefundene Zugangsdaten-Quelle einzeln durchprobieren
setup_mysql_cfg() {
  local err line src user pass probe
  check_mysql_cfg_perms

  if [[ -f "$MYSQL_CFG" ]] && err="$(mysql_conn_error)"; then
    if mysql_cfg_is_ours && grep -q '^password=' -- "$MYSQL_CFG"; then
      local backup; backup="$(mktemp)" && cat -- "$MYSQL_CFG" > "$backup"
      if write_mysql_cfg socket >/dev/null && mysql_conn_error >/dev/null; then
        log "Zugangsdaten aus $MYSQL_CFG entfernt - unix_socket genuegt"
      elif [[ -s "$backup" ]]; then
        cat -- "$backup" > "$MYSQL_CFG"; chmod 0400 -- "$MYSQL_CFG"
        log "vorhanden und funktionsfaehig (mit Zugangsdaten): $MYSQL_CFG"
      fi
      rm -f -- "$backup"
    else
      log "vorhanden und funktionsfaehig: $MYSQL_CFG"
    fi
    return 0
  fi

  if [[ -f "$MYSQL_CFG" ]] && ! mysql_cfg_is_ours; then
    warn "$MYSQL_CFG wurde nicht von diesem Skript erzeugt und funktioniert nicht ($err) - bleibt unveraendert"
    return 1
  fi
  [[ -f "$MYSQL_CFG" ]] && log "$MYSQL_CFG funktioniert nicht - wird neu aufgebaut"

  if write_mysql_cfg socket && err="$(mysql_conn_error)"; then
    log "Anmeldung ueber unix_socket erfolgreich - keine Zugangsdaten noetig"
    return 0
  fi
  log "unix_socket traegt nicht - suche Zugangsdaten auf dem System"

  # Jede Quelle einzeln testen. Panels halten teils veraltete Passwoerter vor,
  # deshalb reicht es nicht, der ersten Fundstelle zu vertrauen.
  # Je Quelle beide Verbindungswege probieren: erst den angegebenen Host
  # (TCP), dann den Socket. Welches der beiden Konten das Passwort traegt,
  # laesst sich von aussen nicht erkennen.
  local h pt way
  while IFS='|' read -r src user pass h pt; do
    [[ -n "$src" && -n "$user" ]] || continue
    for way in tcp socket; do
      if [[ "$way" == "tcp" ]]; then
        [[ -n "$h" && "$h" != "localhost" ]] || continue
        write_mysql_cfg creds "$user" "$pass" "$h" "$pt" >/dev/null || continue
      else
        write_mysql_cfg creds "$user" "$pass" "" "" >/dev/null || continue
      fi
      if mysql_conn_error >/dev/null 2>&1; then
        log "Anmeldung erfolgreich: $src, Benutzer $user, Weg ${way}${h:+ ($h${pt:+:$pt})}"
        return 0
      fi
    done
    log "Zugangsdaten aus $src funktionieren nicht"
  done < <(panel_db_creds_all)

  # Kein Passwort soll nutzlos auf der Platte liegenbleiben.
  write_mysql_cfg socket >/dev/null 2>&1 || true
  warn "keine funktionierende MySQL-Verbindung herstellbar (Details: cmk-agent-update --check-mysql)"
  return 1
}

# "mariadb-admin ping" ist als Test UNGEEIGNET: es liefert Exit 0, sobald der
# Server antwortet - auch bei "Access denied". Deshalb eine echte Abfrage, die
# nur mit gueltiger Anmeldung durchkommt.
mysql_client_bin() {
  local b
  for b in mariadb mysql; do have "$b" && { command -v "$b"; return 0; }; done
  return 1
}

mysql_test_cfg() {  # $1 = Konfigurationsdatei -> Fehlertext auf stdout
  local cfg="$1" cli out
  cli="$(mysql_client_bin)" || { printf 'kein mysql/mariadb-Client gefunden\n'; return 1; }
  out="$(timeout 30 "$cli" --defaults-file="$cfg" -N -B -e 'SELECT 1' 2>&1)"
  if [[ "${out//[[:space:]]/}" == "1" ]]; then
    return 0
  fi
  printf '%s\n' "${out%%$'\n'*}"
  return 1
}

mysql_conn_error() { mysql_test_cfg "$MYSQL_CFG"; }


# Diagnose: zeigt Socket, gefundene Quellen (Passwoerter maskiert) und das
# Ergebnis des Verbindungstests je Quelle. Aendert nichts.
if [[ "${1:-}" == "--check-mysql" ]]; then
  printf 'Socket:  %s\n' "$(mysql_socket || echo '<nicht gefunden>')"
  printf 'Client:  %s\n' "$(mysql_client_bin || echo '<nicht gefunden>')"
  printf 'Config:  %s%s\n' "$MYSQL_CFG" "$([[ -f "$MYSQL_CFG" ]] && echo '' || echo ' (fehlt)')"
  if [[ -f "$MYSQL_CFG" ]]; then
    if err="$(mysql_test_cfg "$MYSQL_CFG")"; then echo '         -> Verbindung OK'
    else printf '         -> %s\n' "$err"; fi
  fi
  echo
  echo 'Gefundene Zugangsdaten-Quellen:'
  tmpcfg="$(mktemp)"; found=0
  while IFS='|' read -r src user pass h pt; do
    [[ -n "$src" ]] || continue
    found=1
    printf '  %-20s user=%-12s pass=%s… (%s Zeichen) host=%s port=%s\n' \
      "$src" "$user" "${pass:0:2}" "${#pass}" "${h:-<socket>}" "${pt:-}"
    for way in tcp socket; do
      if [[ "$way" == "tcp" ]]; then
        [[ -n "$h" && "$h" != "localhost" ]] || continue
        { printf '[client]\n'; printf 'host=%s\nprotocol=tcp\n' "$h"
          [[ -n "$pt" ]] && printf 'port=%s\n' "$pt"
          printf 'user=%s\n' "$user"; printf 'password=%s\n' "$(mycnf_quote "$pass")"; } > "$tmpcfg"
      else
        { printf '[client]\n'; printf 'socket=%s\n' "$(mysql_socket)"
          printf 'user=%s\n' "$user"; printf 'password=%s\n' "$(mycnf_quote "$pass")"; } > "$tmpcfg"
      fi
      if err="$(mysql_test_cfg "$tmpcfg")"; then printf '    via %-6s -> Verbindung OK\n' "$way"
      else printf '    via %-6s -> %s\n' "$way" "$err"; fi
    done
  done < <(panel_db_creds_all)
  rm -f -- "$tmpcfg"
  (( found )) || echo '  (keine)'
  exit 0
fi

########################### Ablauf ##########################################

log "Checkmk-Agent-Update von $CMK_SERVER/$CMK_SITE"

for dl in check_mk_agent check_mk_caching_agent; do
  update_file "$BASE/${dl}.linux" "$BIN_DIR/$dl" 0750 validate_shell || true
done

update_file "$BASE/mk-job" "$BIN_DIR/mk-job" 0750 validate_shell || true
update_file "$BASE/waitmax" "$BIN_DIR/waitmax" 0750 validate_elf 1 /bin/true || true

PLUGIN_TARGET="$PLUGIN_DIR/$PLUGIN_INTERVAL"

# smart nur auf Blech und nur, wenn smartctl ueberhaupt da ist. Nach einer
# P2V-Migration wird die Kopie wieder entfernt, statt dauerhaft Fehler zu
# produzieren.
if ! systemd-detect-virt >/dev/null 2>&1 && command -v smartctl >/dev/null 2>&1; then
  if update_file "$BASE/plugins/smart" "$PLUGIN_TARGET/smart" 0750 validate_shell; then
    purge_other_copies smart "$PLUGIN_TARGET/smart"
  fi
else
  remove_plugin smart
fi

# mk_postgres deckt Sessions, Verbindungen, Locks, Bloat, VACUUM und
# Datenbankstatistiken ab und braucht dank peer-Authentifizierung als Benutzer
# postgres keine Zugangsdaten. Nur installieren, wo auch PostgreSQL laeuft.
if command -v pg_lsclusters >/dev/null 2>&1 || command -v psql >/dev/null 2>&1 \
   || systemctl list-units --type=service --state=running 2>/dev/null | grep -q 'postgresql'; then
  if update_file "$BASE/plugins/mk_postgres.py" "$PLUGIN_TARGET/mk_postgres.py" 0750 validate_python; then
    purge_other_copies mk_postgres.py "$PLUGIN_TARGET/mk_postgres.py"
  fi
else
  remove_plugin mk_postgres.py
fi

# mk_mysql deckt Ping, globale Status- und Variablenwerte, Datenbankgroessen
# und Replikation ab.
if mysql_present; then
  if ! setup_mysql_cfg; then
    fail "mk_mysql: keine funktionierende Verbindung ueber $MYSQL_CFG - Plugin nicht installiert"
  elif update_file "$BASE/plugins/mk_mysql" "$PLUGIN_TARGET/mk_mysql" 0750 validate_shell; then
    purge_other_copies mk_mysql "$PLUGIN_TARGET/mk_mysql"
  fi
else
  remove_plugin mk_mysql
fi

if [[ -x "$PLUGIN_MANAGER" ]]; then
  log "Aktualisiere Plugin-Verlinkung"
  "$PLUGIN_MANAGER" || fail "Plugin-Manager meldete einen Fehler"
else
  warn "$PLUGIN_MANAGER nicht ausfuehrbar - Verlinkung uebersprungen"
fi

if (( FAILED > 0 )); then
  warn "$FAILED Schritt(e) fehlgeschlagen - betroffene Dateien wurden NICHT ersetzt"
  exit 1
fi
log "Update abgeschlossen"
exit 0