livekit-ip-watch/livekit-ip-watch.v1.sh
2025-10-27 19:07:55 +01:00

312 lines
8.4 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 "$@"