diff --git a/livekit-ip-watch.v1.sh b/livekit-ip-watch.v1.sh new file mode 100644 index 0000000..cf2f1fd --- /dev/null +++ b/livekit-ip-watch.v1.sh @@ -0,0 +1,311 @@ +#!/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: "http" oder "tcp" +HEALTHCHECK_MODE="http" +HEALTHCHECK_URL="https://rtc.matrix.techniverse.net" +TCP_HOST="127.0.0.1" +TCP_PORT="7880" +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; in_includes=0; injected=0 } + { + line=$0 + if ($0 ~ /^[[:space:]]*rtc:[[:space:]]*$/) { in_rtc=1; in_ips=0; in_includes=0 } + else if (in_rtc && $0 ~ /^[[:space:]]*[[:alnum:]_]+:[[:space:]]*$/ && $0 !~ /^[[:space:]]*ips:/) { + if (!in_ips && !in_includes && !injected) { + print " ips:" + print " includes:" + print " - " newcidr + injected=1 + } + } + if (in_rtc && $0 ~ /^[[:space:]]*ips:[[:space:]]*$/) { in_ips=1; in_includes=0 } + if (in_ips && $0 ~ /^[[:space:]]*includes:[[:space:]]*$/) { in_includes=1; next } + if (in_includes) { + if ($0 ~ /^[[:space:]]*-[[:space:]]*[^[:space:]]+/) { + next + } else { + if (!injected) { + print " - " newcidr + injected=1 + } + in_includes=0 + in_ips=($0 ~ /^[[:space:]]*ips:/)?1:0 + } + } + print line + } + END{ + if (in_rtc && !injected) { + 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_http() { + curl -fsS --max-time "$HEALTHCHECK_TIMEOUT" "$HEALTHCHECK_URL" >/dev/null +} + +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 +} + +do_healthcheck() { + if [[ "$HEALTHCHECK_MODE" == "http" ]]; then + healthcheck_http + else + healthcheck_tcp + 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 + + # Aktuelle IP ermitteln + 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" + + # YAML aktualisieren + 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 + + # Dienst neu starten + info "Restart des Dienstes..." + restart_service + + info "Warte $WAIT_AFTER_RESTART Sekunden..." + sleep "$WAIT_AFTER_RESTART" + + # Healthcheck + if do_healthcheck; then + info "Healthcheck OK." + echo "$current_ip" > "$state_file" + notify "info" "IP geändert auf $current_ip, Dienst gesund." + exit 0 + fi + + # Rollback bei Fehler + 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 do_healthcheck; 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 "$@"