#!/usr/bin/env bash # Beschreibung: Aktualisiert livekit.yaml (rtc.ips.includes) bei IP-Wechsel (IPv4 ODER IPv6), # restarte den LiveKit-Dienst (Docker oder Docker Compose), wartet konfigurierbar und prüft Health. # Mit Backup + Rollback, optionalen ntfy-Notifications, YAML-Update via yq (Fallback eingebaut). # Synapse: https://git.techniverse.net/scriptos/livekit-ip-watch.git # Autor: Patrick Asmus # Web: https://www.cleveradmin.de # Version: 1.0 # Datum: 27.10.2025 # Modifikation: Initial ##################################################### set -euo pipefail ############################ # Konfiguration (Variablen) ############################ # Pfad zur LiveKit-Konfiguration CONFIG_FILE="/home/docker-container/matrix-rtc/data/matrix-element-call-livekit/config.yaml" # Betriebsmodus: "compose" oder "docker" RUNTIME="compose" # Compose: Kompletter Pfad zur Compose-Datei + Service-Name COMPOSE_FILE_PATH="/home/docker-container/matrix-rtc/docker-compose.yaml" COMPOSE_SERVICE="matrix-element-call-livekit" # Plain-Docker: Container-Name (nur genutzt, wenn RUNTIME="docker") CONTAINER_NAME="matrix-element-call-livekit" # IP-Variante: IPv4 oder IPv6 ENABLE_IPV6=false # Wartezeit nach Neustart (Sekunden), dann Healthcheck WAIT_AFTER_RESTART=20 # Healthcheck-Konfiguration: nur TCP TCP_HOST="127.0.0.1" TCP_PORT="7881" HEALTHCHECK_TIMEOUT=3 # ntfy (optional) NTFY_URL="" NTFY_TOKEN="" ############################ # Hauptscript ############################ # Skriptverzeichnis, Lock- und State-Dateien SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LOCKFILE="$SCRIPT_DIR/livekit-ip-watch.lock" STATE_FILE_V4="$SCRIPT_DIR/last_ip_v4" STATE_FILE_V6="$SCRIPT_DIR/last_ip_v6" BACKUP_DIR="$SCRIPT_DIR/backups" mkdir -p "$BACKUP_DIR" log() { printf '[%(%Y-%m-%d %H:%M:%S)T] %s\n' -1 "$*" >&2; } die() { log "ERROR: $*"; notify "error" "$*"; exit 1; } warn() { log "WARN: $*"; notify "warn" "$*"; } info() { log "INFO: $*"; } # Ntfy notify() { local level="${1:-info}" shift || true local msg="${*:-}" [[ -z "$NTFY_URL" ]] && return 0 local hdr=(-H "Title: livekit-ip-watch" -H "Priority: default" -H "Tags: $level") [[ -n "$NTFY_TOKEN" ]] && hdr+=(-H "Authorization: Bearer $NTFY_TOKEN") curl -fsS -X POST "${hdr[@]}" -d "$msg" "$NTFY_URL" >/dev/null 2>&1 || true } # Lock acquire_lock() { if command -v flock >/dev/null 2>&1; then exec 9>"$LOCKFILE" if ! flock -n 9; then info "Läuft bereits (Lock vorhanden): $LOCKFILE" exit 0 fi else set -o noclobber if ! : > "$LOCKFILE"; then info "Läuft bereits (Lock vorhanden): $LOCKFILE" exit 0 fi set +o noclobber fi trap 'release_lock' EXIT } release_lock() { rm -f "$LOCKFILE" 2>/dev/null || true } # IP-Ermittlung get_ipv4() { local ip ip="$(dig +short myip.opendns.com @resolver1.opendns.com 2>/dev/null || true)" [[ -z "$ip" ]] && ip="$(curl -fsS https://api.ipify.org 2>/dev/null || true)" [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || ip="" echo "$ip" } get_ipv6() { local ip ip="$(curl -fsS -6 https://api6.ipify.org 2>/dev/null || true)" if [[ "$ip" =~ : ]]; then echo "$ip" else echo "" fi } have_yq() { command -v yq >/dev/null 2>&1; } backup_config() { [[ -r "$CONFIG_FILE" ]] || die "Config nicht lesbar: $CONFIG_FILE" local ts backup ts="$(date +%Y%m%d%H%M%S)" backup="$BACKUP_DIR/$(basename "$CONFIG_FILE").$ts.bak" cp -a "$CONFIG_FILE" "$backup" echo "$backup" } update_yaml_with_yq() { local cidr="$1" YQ_VAR_CIDR="$cidr" yq -i ' .rtc |= (. // {}) | .rtc.ips |= (. // {}) | .rtc.ips.includes = [env(YQ_VAR_CIDR)] ' "$CONFIG_FILE" } update_yaml_fallback() { local cidr="$1" local tmp tmp="$(mktemp)" awk -v newcidr="$cidr" ' BEGIN{ in_rtc=0; in_ips=0; ips_indent=""; printed_ips=0 } function print_ips_block() { if (!printed_ips) { print " ips:" print " includes:" print " - " newcidr printed_ips=1 } } { line=$0 if ($0 ~ /^[[:space:]]*rtc:[[:space:]]*$/) { in_rtc=1; in_ips=0; ips_indent=""; print; next } if (in_rtc) { if (in_ips) { if (ips_indent=="" ) { next } if ($0 ~ "^" ips_indent "[^[:space:]]") { next } in_ips=0 print_ips_block() } if ($0 ~ /^[[:space:]]{2}ips:[[:space:]]*$/) { in_ips=1 match($0,/^([[:space:]]*)ips:/,m) ips_indent=m[1]" " next } if ($0 ~ /^[[:space:]]{2}[[:alnum:]_]+:[[:space:]]*$/) { if (!printed_ips) { print_ips_block() } } if ($0 ~ /^[[:alnum:]_]+:[[:space:]]*$/) { if (!printed_ips && in_rtc) { print_ips_block() } in_rtc=0 } print line next } print line } END{ if (in_rtc && !printed_ips) { print " ips:" print " includes:" print " - " newcidr } } ' "$CONFIG_FILE" > "$tmp" mv "$tmp" "$CONFIG_FILE" } detect_compose_cmd() { if command -v docker-compose >/dev/null 2>&1; then echo "docker-compose" else echo "docker compose" fi } restart_service() { if [[ "$RUNTIME" == "compose" ]]; then local dir file dir="$(dirname "$COMPOSE_FILE_PATH")" file="$(basename "$COMPOSE_FILE_PATH")" local COMPOSE_CMD COMPOSE_CMD="$(detect_compose_cmd)" ( cd "$dir" && "$COMPOSE_CMD" -f "$file" restart "$COMPOSE_SERVICE" ) else docker restart "$CONTAINER_NAME" fi } healthcheck_tcp() { if (exec 3<>/dev/tcp/"$TCP_HOST"/"$TCP_PORT") 2>/dev/null; then exec 3<&- exec 3>&- return 0 elif command -v nc >/dev/null 2>&1; then nc -z -w "$HEALTHCHECK_TIMEOUT" "$TCP_HOST" "$TCP_PORT" else return 1 fi } main() { acquire_lock [[ -f "$CONFIG_FILE" ]] || die "Config-Datei nicht gefunden: $CONFIG_FILE" if [[ "$RUNTIME" == "compose" ]]; then [[ -f "$COMPOSE_FILE_PATH" ]] || die "Compose-Datei nicht gefunden: $COMPOSE_FILE_PATH" [[ -n "$COMPOSE_SERVICE" ]] || die "COMPOSE_SERVICE ist leer." else [[ -n "$CONTAINER_NAME" ]] || die "CONTAINER_NAME ist leer." fi local current_ip cidr state_file if [[ "$ENABLE_IPV6" == true ]]; then current_ip="$(get_ipv6)" [[ -z "$current_ip" ]] && die "Konnte keine öffentliche IPv6 ermitteln." cidr="${current_ip}/128" state_file="$STATE_FILE_V6" else current_ip="$(get_ipv4)" [[ -z "$current_ip" ]] && die "Konnte keine öffentliche IPv4 ermitteln." cidr="${current_ip}/32" state_file="$STATE_FILE_V4" fi local last_ip="" [[ -f "$state_file" ]] && last_ip="$(cat "$state_file" 2>/dev/null || true)" if [[ "$current_ip" == "$last_ip" && -n "$last_ip" ]]; then info "IP unverändert: $current_ip" notify "info" "Keine Änderung. Öffentliche IP bleibt $current_ip." exit 0 fi info "Neue IP erkannt: $current_ip (alt: ${last_ip:-none})" local backup backup="$(backup_config)" info "Backup erstellt: $backup" if have_yq; then info "Aktualisiere YAML mit yq → .rtc.ips.includes = [ \"$cidr\" ]" update_yaml_with_yq "$cidr" else warn "yq nicht gefunden – Fallback-Editor wird verwendet." update_yaml_fallback "$cidr" fi info "Restart des Dienstes..." restart_service info "Warte $WAIT_AFTER_RESTART Sekunden..." sleep "$WAIT_AFTER_RESTART" if healthcheck_tcp; then info "Healthcheck OK." echo "$current_ip" > "$state_file" notify "info" "IP geändert auf $current_ip, Dienst gesund." exit 0 fi warn "Healthcheck fehlgeschlagen – führe Rollback durch." cp -a "$backup" "$CONFIG_FILE" info "Rollback-Konfiguration wiederhergestellt: $backup" info "Restart nach Rollback..." restart_service info "Warte $WAIT_AFTER_RESTART Sekunden (Rollback)..." sleep "$WAIT_AFTER_RESTART" if healthcheck_tcp; then warn "Rollback erfolgreich. System läuft wieder mit alter Konfiguration." notify "warn" "Rollback erfolgreich. Bitte prüfen. Alte IP bleibt ${last_ip:-unbekannt}." exit 10 else die "Rollback fehlgeschlagen. Manuelles Eingreifen erforderlich." fi } main "$@"