#!/bin/sh set -eu . /usr/libexec/haproxy-manager/common.sh umask 077 ACTION="$(printf '%s' "${1:-network}" | tr -cd 'A-Za-z0-9_.:-')" INTERFACE="$(printf '%s' "${2:-unknown}" | tr -cd 'A-Za-z0-9_.:-')" RECOVER_LOCK=/var/lock/haproxy-manager.recover APPLY_LOCK=/var/lock/haproxy-manager.apply APPLY_LOCK_HELD=0 TMP_CFG="/tmp/haproxy-manager-recover.$$" INCIDENT="" BASE="" DIAGNOSTICS=/dev/null [ -n "$ACTION" ] || ACTION=network [ -n "$INTERFACE" ] || INTERFACE=unknown [ "$(uci_get main auto_recover 1)" = 1 ] || exit 0 [ "$(uci_get main enabled 0)" = 1 ] || exit 0 RECONCILE_GENERATED=0 case "$ACTION" in ifup|ifupdate) [ "$INTERFACE" = "$(wan_interface)" ] || exit 0 RECONCILE_GENERATED=1 ;; esac [ -x /etc/init.d/haproxy ] || exit 0 /etc/init.d/haproxy enabled >/dev/null 2>&1 || exit 0 MODE="$(active_mode)" case "$MODE" in generated|raw) ;; *) exit 0 ;; esac cleanup() { rm -f "$TMP_CFG" [ "$APPLY_LOCK_HELD" != 1 ] || operation_lock_release "$APPLY_LOCK" operation_lock_release "$RECOVER_LOCK" } trim_incidents() { local count old count=0 for old in $(find "$BASE" -maxdepth 1 -type d -name '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]' 2>/dev/null | sort -r); do count=$((count + 1)) [ "$count" -le "$INCIDENT_LIMIT" ] && continue case "$old" in "$BASE"/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) rm -f "$old/meta" "$old/result" "$old/diagnostics.log" rmdir "$old" 2>/dev/null || true ;; esac done } create_incident() { local reason="$1" id attempt target_ip requested_base requested_base="$(incident_dir)" || return 1 umask 077 mkdir -p "$requested_base" || { logger -t haproxy-manager "Cannot create incident report directory: $requested_base" return 1 } BASE="$(resolve_storage_dir "$requested_base")" || { logger -t haproxy-manager "Incident directory resolves outside /root or /mnt" return 1 } id="$(date +%Y%m%d-%H%M%S)" INCIDENT="$BASE/$id" attempt=1 while ! mkdir "$INCIDENT" 2>/dev/null; do [ "$attempt" -lt 5 ] || { logger -t haproxy-manager "Cannot create incident report in $BASE" return 1 } sleep 1 attempt=$((attempt + 1)) id="$(date +%Y%m%d-%H%M%S)" INCIDENT="$BASE/$id" done chmod 700 "$BASE" "$INCIDENT" 2>/dev/null || true target_ip="$(wan_ip 2>/dev/null || echo unknown)" cat > "$INCIDENT/meta" < "$INCIDENT/result" || return 1 printf '%s\n' "$id" > "$BASE/LAST" || return 1 trim_incidents DIAGNOSTICS="$INCIDENT/diagnostics.log" if [ "$reason" = wan-address-change ]; then { printf 'HAProxy Manager WAN listener reconciliation\n' printf 'Incident: %s\nAction: %s\nInterface: %s\nReason: %s\nTarget WAN address: %s\n' \ "$id" "$ACTION" "$INTERFACE" "$reason" "$target_ip" } > "$DIAGNOSTICS" 2>&1 return 0 fi { printf 'HAProxy Manager automatic recovery\n' printf 'Incident: %s\nAction: %s\nInterface: %s\nReason: %s\nTarget WAN address: %s\n\n' \ "$id" "$ACTION" "$INTERFACE" "$reason" "$target_ip" printf '== Time and uptime ==\n' date || true uptime || true printf '\n== Memory ==\n' free || true printf '\n== IPv4 addresses ==\n' ip -4 addr show || true printf '\n== HAProxy procd state ==\n' ubus call service list '{"name":"haproxy","verbose":true}' || true printf '\n== Network interfaces ==\n' ubus call network.interface dump || true printf '\n== Recent HAProxy, procd, and OOM messages ==\n' logread | grep -iE 'haproxy|procd.*instance|out of memory|oom|killed process' | tail -n 160 || true printf '\n== Kernel OOM messages ==\n' dmesg | grep -Ei 'oom|out of memory|killed process|haproxy' | tail -n 80 || true } > "$DIAGNOSTICS" 2>&1 } set_result() { local result="$1" id reason if [ -z "$INCIDENT" ] || [ ! -d "$INCIDENT" ]; then logger -t haproxy-manager "Recovery result without incident report: $result" return 0 fi if ! printf '%s\n' "$result" > "$INCIDENT/result"; then logger -t haproxy-manager "Could not update incident report with result: $result" INCIDENT="" DIAGNOSTICS=/dev/null return 0 fi id="${INCIDENT##*/}" reason="$(sed -n 's/^reason=//p' "$INCIDENT/meta" 2>/dev/null | head -1)" ( if ! /usr/libexec/haproxy-manager/notify "$id" "$result" "$ACTION" "$INTERFACE" "$reason"; then logger -t haproxy-manager "Webhook notification failed for incident $id" fi ) >/dev/null 2>&1 & } operation_lock_acquire "$RECOVER_LOCK" || exit 0 trap cleanup EXIT # Let netifd finish installing addresses and routes before comparing listeners. sleep 2 operation_lock_acquire "$APPLY_LOCK" || exit 0 APPLY_LOCK_HELD=1 CONFIG_CHANGED=0 GENERATION_FAILED=0 VALIDATION_FAILED=0 if [ "$RECONCILE_GENERATED" = 1 ] && [ "$MODE" = generated ] && [ "$(uci_get main wan_bind_ip auto)" = auto ]; then if /usr/libexec/haproxy-manager/generate "$TMP_CFG" >/dev/null 2>&1; then if /usr/libexec/haproxy-manager/validate "$TMP_CFG" >/dev/null 2>&1; then cmp -s "$TMP_CFG" "$HAPROXY_CFG" || CONFIG_CHANGED=1 else VALIDATION_FAILED=1 fi else GENERATION_FAILED=1 fi fi if [ "$GENERATION_FAILED" != 1 ] && [ "$VALIDATION_FAILED" != 1 ] && [ "$CONFIG_CHANGED" != 1 ] && /etc/init.d/haproxy status >/dev/null 2>&1; then exit 0 fi if [ "$GENERATION_FAILED" = 1 ]; then REASON=wan-address-unavailable elif [ "$VALIDATION_FAILED" = 1 ]; then REASON=invalid-generated-config elif [ "$CONFIG_CHANGED" = 1 ]; then REASON=wan-address-change else REASON=service-stopped fi if ! create_incident "$REASON"; then logger -t haproxy-manager "Incident report could not be stored; recovery will continue" INCIDENT="" DIAGNOSTICS=/dev/null fi logger -t haproxy-manager "HAProxy recovery started after $ACTION on $INTERFACE ($REASON)" if [ "$GENERATION_FAILED" = 1 ]; then set_result address-unavailable logger -t haproxy-manager "Automatic recovery stopped: WAN address is unavailable" exit 1 fi if [ "$VALIDATION_FAILED" = 1 ]; then set_result invalid-generated-config logger -t haproxy-manager "Automatic recovery stopped: generated configuration is invalid" exit 1 fi if [ "$CONFIG_CHANGED" = 1 ]; then BACKUP_PATH="" if ! BACKUP_PATH="$(/usr/libexec/haproxy-manager/backup 2>> "$DIAGNOSTICS")"; then set_result backup-failed logger -t haproxy-manager "Automatic recovery stopped: snapshot creation failed" exit 1 fi printf '\nRecovery point: %s\n' "${BACKUP_PATH##*/}" >> "$DIAGNOSTICS" || true if ! atomic_install_file "$TMP_CFG" "$HAPROXY_CFG"; then set_result install-failed logger -t haproxy-manager "Automatic recovery stopped: HAProxy configuration could not be installed" exit 1 fi if /etc/init.d/haproxy restart >> "$DIAGNOSTICS" 2>&1 && /etc/init.d/haproxy status >/dev/null 2>&1; then set_result reconciled logger -t haproxy-manager "HAProxy listeners reconciled with the current WAN address" exit 0 fi if /usr/libexec/haproxy-manager/rollback --sync "${BACKUP_PATH##*/}" >> "$DIAGNOSTICS" 2>&1; then set_result rolled-back logger -t haproxy-manager "WAN listener reconciliation failed and was rolled back" else set_result rollback-failed logger -t haproxy-manager "WAN listener reconciliation and rollback both failed" fi exit 1 fi if ! /etc/init.d/haproxy check >> "$DIAGNOSTICS" 2>&1; then set_result invalid-config logger -t haproxy-manager "Automatic recovery stopped: HAProxy configuration is invalid" exit 1 fi attempt=1 while [ "$attempt" -le 5 ]; do printf '\n== Start attempt %s ==\n' "$attempt" >> "$DIAGNOSTICS" || true /etc/init.d/haproxy restart >> "$DIAGNOSTICS" 2>&1 || true sleep $((attempt + 1)) if /etc/init.d/haproxy status >/dev/null 2>&1; then set_result recovered { printf '\n== Recovered listeners ==\n' ps w | grep '[h]aproxy' || true netstat -lntp 2>/dev/null | grep haproxy || true } >> "$DIAGNOSTICS" 2>&1 logger -t haproxy-manager "HAProxy recovered after $attempt attempt(s)" exit 0 fi attempt=$((attempt + 1)) done set_result failed logger -t haproxy-manager "HAProxy automatic recovery failed after 5 attempts" exit 1