livekit-ip-watch/livekit-ip-watch.v1.sh

285 lines
7.9 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.1
# Datum: 27.10.2025
# Modifikation: healthcheck auf HTTP mit Custom URL umgestellt.
#####################################################
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
HEALTHCHECK_DOMAIN="rtc.matrix.techniverse.net"
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() {
local url="https://${HEALTHCHECK_DOMAIN}/livekit/sfu/"
curl -fsS --max-time "$HEALTHCHECK_TIMEOUT" "$url" >/dev/null
}
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_http; 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_http; 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 "$@"