From a537fc64697c618248f9c6487db9f35f5df0b3ee Mon Sep 17 00:00:00 2001 From: sorcier Date: Thu, 5 Nov 2026 23:28:53 +0100 Subject: [PATCH] fix(cluster): ensure AstDB transactional sync, auto-healing, and Gratuitous ARP on master take-over --- install.sh | 2 +- scripts/cluster/notify_master.sh | 25 ++++++++++--- scripts/nexus-cluster | 60 ++++++++++++++++++++++++++++++-- src/ClusterManager.php | 17 ++++++--- 4 files changed, 91 insertions(+), 13 deletions(-) diff --git a/install.sh b/install.sh index 1351280..92aafed 100755 --- a/install.sh +++ b/install.sh @@ -65,7 +65,7 @@ apt-get install -y --no-install-recommends \ sshpass # Composants optionnels Haute Disponibilité (Cluster HA) -apt-get install -y --no-install-recommends keepalived 2>/dev/null || true +apt-get install -y --no-install-recommends keepalived iputils-arping 2>/dev/null || true systemctl stop keepalived >/dev/null 2>&1 || true systemctl disable keepalived >/dev/null 2>&1 || true diff --git a/scripts/cluster/notify_master.sh b/scripts/cluster/notify_master.sh index 12713c9..3a0bc68 100755 --- a/scripts/cluster/notify_master.sh +++ b/scripts/cluster/notify_master.sh @@ -12,11 +12,26 @@ echo "[$TIMESTAMP] [CLUSTER] >>> Transition vers l'état MASTER (VIP active) <<< # 1. Mise à jour de l'état en base de données locale si SQLite disponible DB_PATH="/var/www/nexus-pbx/storage/database.sqlite" +ASTDB_PATH="/var/lib/asterisk/astdb.sqlite3" + if [ -f "$DB_PATH" ]; then sqlite3 "$DB_PATH" "UPDATE cluster_settings SET last_sync_status='master_active', updated_at=CURRENT_TIMESTAMP WHERE id=1;" 2>/dev/null || true fi -# 2. Réactivation des enregistrements Trunks PJSIP et rechargement de la config +# 2. Vérification et réparation d'intégrité de la base SQLite AstDB (contacts téléphones) +if [ -f "$ASTDB_PATH" ]; then + INTEGRITY=$(sqlite3 "$ASTDB_PATH" "PRAGMA integrity_check;" 2>/dev/null || echo "corrupt") + if [ "$INTEGRITY" != "ok" ]; then + echo "[$TIMESTAMP] [CLUSTER] ALERTE: astdb.sqlite3 corrompu ($INTEGRITY). Auto-réparation immédiate..." >> "$LOG_FILE" + mv -f "$ASTDB_PATH" "${ASTDB_PATH}.corrupt.$(date +%s)" 2>/dev/null || true + sqlite3 "$ASTDB_PATH" "CREATE TABLE astdb(key TEXT, value TEXT, PRIMARY KEY(key));" 2>/dev/null || true + chown asterisk:asterisk "$ASTDB_PATH" 2>/dev/null || true + chmod 664 "$ASTDB_PATH" 2>/dev/null || true + echo "[$TIMESTAMP] [CLUSTER] astdb.sqlite3 recréé proprement." >> "$LOG_FILE" + fi +fi + +# 3. Réactivation des enregistrements Trunks PJSIP et rechargement de la config if pidof asterisk >/dev/null 2>&1; then /usr/sbin/asterisk -rx "pjsip reload" >> "$LOG_FILE" 2>&1 || true /usr/sbin/asterisk -rx "pjsip send register *" >> "$LOG_FILE" 2>&1 || true @@ -26,11 +41,13 @@ if pidof asterisk >/dev/null 2>&1; then /usr/sbin/asterisk -rx "dialplan reload" >> "$LOG_FILE" 2>&1 || true fi -# 3. Gratuitous ARP optionnel si arping est présent +# 4. Gratuitous ARP pour forcer la mise à jour immédiate des tables ARP des switches et téléphones SIP VIP=$(sqlite3 "$DB_PATH" "SELECT virtual_ip FROM cluster_settings WHERE id=1;" 2>/dev/null || echo "") IFACE=$(sqlite3 "$DB_PATH" "SELECT interface FROM cluster_settings WHERE id=1;" 2>/dev/null || echo "eth0") -if [ -n "$VIP" ] && command -v arping >/dev/null 2>&1; then - arping -c 3 -U -I "$IFACE" "$VIP" >> "$LOG_FILE" 2>&1 || true +VIP_CLEAN=$(echo "$VIP" | cut -d'/' -f1) +if [ -n "$VIP_CLEAN" ] && command -v arping >/dev/null 2>&1; then + echo "[$TIMESTAMP] [CLUSTER] Émission Gratuitous ARP pour $VIP_CLEAN sur $IFACE..." >> "$LOG_FILE" + arping -c 5 -U -I "$IFACE" "$VIP_CLEAN" >> "$LOG_FILE" 2>&1 || true fi echo "[$TIMESTAMP] [CLUSTER] Prise de relais MASTER terminée avec succès." >> "$LOG_FILE" diff --git a/scripts/nexus-cluster b/scripts/nexus-cluster index 306d735..ac0eb49 100755 --- a/scripts/nexus-cluster +++ b/scripts/nexus-cluster @@ -15,9 +15,9 @@ case "$CMD" in echo "net.ipv4.ip_nonlocal_bind=1" > /etc/sysctl.d/99-nexus-ha.conf 2>/dev/null || true fi - # 2. Vérification et installation éventuelle de Keepalived - if ! command -v keepalived >/dev/null 2>&1; then - apt-get update -qq && apt-get install -y -qq keepalived >/dev/null 2>&1 || true + # 2. Vérification et installation éventuelle de Keepalived et arping + if ! command -v keepalived >/dev/null 2>&1 || ! command -v arping >/dev/null 2>&1; then + apt-get update -qq && apt-get install -y -qq keepalived iputils-arping >/dev/null 2>&1 || true fi # 3. Création et sécurisation stricte du dossier des scripts Keepalived (exigences strictes Keepalived 2.x) @@ -64,6 +64,60 @@ case "$CMD" in fi ;; + restore-astdb) + SRC="${2:-/tmp/astdb_cluster_sync.sqlite3}" + DEST="/var/lib/asterisk/astdb.sqlite3" + if [ ! -f "$SRC" ] || [ ! -s "$SRC" ]; then + echo "ASTDB_RESTORE_NO_FILE" + exit 0 + fi + + # Vérification intégrité de la source + SRC_INTEGRITY=$(sqlite3 "$SRC" "PRAGMA integrity_check;" 2>/dev/null || echo "corrupt") + if [ "$SRC_INTEGRITY" != "ok" ]; then + echo "ASTDB_SOURCE_CORRUPT: $SRC_INTEGRITY" + rm -f "$SRC" + exit 1 + fi + + # Fusion ou remplacement propre + if [ -f "$DEST" ]; then + DEST_INTEGRITY=$(sqlite3 "$DEST" "PRAGMA integrity_check;" 2>/dev/null || echo "corrupt") + if [ "$DEST_INTEGRITY" = "ok" ]; then + # Fusion SQLite transactionnelle sans fermer les descripteurs d'Asterisk + sqlite3 "$DEST" "ATTACH '$SRC' AS src; INSERT OR REPLACE INTO astdb SELECT * FROM src.astdb; DETACH src;" 2>/dev/null || cp -f "$SRC" "$DEST" + else + cp -f "$SRC" "$DEST" + fi + else + cp -f "$SRC" "$DEST" + fi + + chown asterisk:asterisk "$DEST" 2>/dev/null || true + chmod 664 "$DEST" 2>/dev/null || true + rm -f "$SRC" + echo "ASTDB_RESTORED_OK" + ;; + + fix-astdb) + DEST="/var/lib/asterisk/astdb.sqlite3" + if [ -f "$DEST" ]; then + INTEGRITY=$(sqlite3 "$DEST" "PRAGMA integrity_check;" 2>/dev/null || echo "corrupt") + if [ "$INTEGRITY" != "ok" ]; then + mv -f "$DEST" "${DEST}.corrupt.$(date +%s)" 2>/dev/null || true + sqlite3 "$DEST" "CREATE TABLE astdb(key TEXT, value TEXT, PRIMARY KEY(key));" 2>/dev/null || true + chown asterisk:asterisk "$DEST" 2>/dev/null || true + chmod 664 "$DEST" 2>/dev/null || true + if pidof asterisk >/dev/null 2>&1; then + /usr/sbin/asterisk -rx "pjsip reload" 2>/dev/null || true + fi + echo "ASTDB_FIXED" + exit 0 + fi + fi + echo "ASTDB_OK" + ;; + check) if pidof asterisk >/dev/null 2>&1 && /usr/sbin/asterisk -rx "core show uptime" 2>/dev/null | grep -qi "uptime"; then echo "HEALTHY" diff --git a/src/ClusterManager.php b/src/ClusterManager.php index 462b7cc..8a9d782 100644 --- a/src/ClusterManager.php +++ b/src/ClusterManager.php @@ -353,6 +353,9 @@ class ClusterManager $conf[] = " virtual_router_id {$vrid}"; $conf[] = " priority {$prio}"; $conf[] = " advert_int 1"; + $conf[] = " garp_master_delay 1"; + $conf[] = " garp_master_repeat 5"; + $conf[] = " garp_master_refresh 5"; $conf[] = ""; // Unicast VRRP si l'IP du nœud pair est configurée (indispensable en environnement virtualisé type Proxmox/VMware) @@ -473,7 +476,12 @@ class ClusterManager @unlink($tmpAstdb); shell_exec("sqlite3 " . escapeshellarg($astdbPath) . " \".backup " . escapeshellarg($tmpAstdb) . "\" 2>/dev/null || cp -f " . escapeshellarg($astdbPath) . " " . escapeshellarg($tmpAstdb) . " 2>/dev/null || true"); if (file_exists($tmpAstdb) && filesize($tmpAstdb) > 0) { - $hasAstdb = true; + $integrity = trim(shell_exec("sqlite3 " . escapeshellarg($tmpAstdb) . " 'PRAGMA integrity_check;' 2>/dev/null") ?? ''); + if ($integrity === 'ok') { + $hasAstdb = true; + } else { + @unlink($tmpAstdb); + } } } @@ -621,10 +629,9 @@ class ClusterManager // Restauration de l'astdb (contacts et IP actives des téléphones PJSIP) shell_exec("tar -xzf " . escapeshellarg($tmpArchive) . " -C /tmp astdb_cluster_sync.sqlite3 2>/dev/null || true"); if (file_exists('/tmp/astdb_cluster_sync.sqlite3')) { - copy('/tmp/astdb_cluster_sync.sqlite3', '/var/lib/asterisk/astdb.sqlite3'); - @chown('/var/lib/asterisk/astdb.sqlite3', 'asterisk'); - @chgrp('/var/lib/asterisk/astdb.sqlite3', 'asterisk'); - @chmod('/var/lib/asterisk/astdb.sqlite3', 0664); + // Utilisation du helper root pour fusionner de manière transactionnelle via SQLite ATTACH + // sans casser les descripteurs de fichiers d'Asterisk ni risquer d'écrasement corrompu + shell_exec('sudo -n ' . escapeshellcmd(self::CLUSTER_HELPER_PATH) . ' restore-astdb /tmp/astdb_cluster_sync.sqlite3 2>&1'); @unlink('/tmp/astdb_cluster_sync.sqlite3'); } @unlink($tmpArchive);