312 lines
8.4 KiB
Bash
312 lines
8.4 KiB
Bash
#!/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 "$@"
|