feat(cluster): production-grade atomic VACUUM replication, pre-validation integrity check, Asterisk database query injection, and live reload
This commit is contained in:
1 parent
a537fc6469
commit
0b740da87e
2 files changed
+98
-29
No files matched your search
+14
-12
@@ -72,7 +72,7 @@ case "$CMD" in
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Vérification intégrité de la source
|
||||
# 1. Vérification intégrité du fichier source temporaire
|
||||
SRC_INTEGRITY=$(sqlite3 "$SRC" "PRAGMA integrity_check;" 2>/dev/null || echo "corrupt")
|
||||
if [ "$SRC_INTEGRITY" != "ok" ]; then
|
||||
echo "ASTDB_SOURCE_CORRUPT: $SRC_INTEGRITY"
|
||||
@@ -80,21 +80,23 @@ case "$CMD" in
|
||||
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
|
||||
chmod 644 "$SRC" 2>/dev/null || true
|
||||
|
||||
# 2. Si Asterisk tourne, injection via son moteur interne database query
|
||||
# Cela garantit l'absence totale de verrou SQLite (database is locked) et préserve les descripteurs ouverts
|
||||
if pidof asterisk >/dev/null 2>&1; then
|
||||
/usr/sbin/asterisk -rx "database query \"ATTACH '$SRC' AS src;\"" >/dev/null 2>&1 || true
|
||||
/usr/sbin/asterisk -rx "database query \"DELETE FROM astdb WHERE key LIKE '/registrar/%';\"" >/dev/null 2>&1 || true
|
||||
/usr/sbin/asterisk -rx "database query \"INSERT INTO astdb SELECT * FROM src.astdb WHERE key LIKE '/registrar/%';\"" >/dev/null 2>&1 || true
|
||||
/usr/sbin/asterisk -rx "database query \"INSERT OR REPLACE INTO astdb SELECT * FROM src.astdb WHERE key NOT LIKE '/registrar/%';\"" >/dev/null 2>&1 || true
|
||||
/usr/sbin/asterisk -rx "database query \"DETACH src;\"" >/dev/null 2>&1 || true
|
||||
else
|
||||
# Asterisk arrêté : copie directe intègre
|
||||
cp -f "$SRC" "$DEST"
|
||||
chown asterisk:asterisk "$DEST" 2>/dev/null || true
|
||||
chmod 664 "$DEST" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
chown asterisk:asterisk "$DEST" 2>/dev/null || true
|
||||
chmod 664 "$DEST" 2>/dev/null || true
|
||||
rm -f "$SRC"
|
||||
echo "ASTDB_RESTORED_OK"
|
||||
;;
|
||||
|
||||
+84
-17
@@ -463,8 +463,25 @@ class ClusterManager
|
||||
throw new RuntimeException("Fichier de base de données introuvable : {$dbPath}");
|
||||
}
|
||||
|
||||
// Forcer le checkpoint SQLite WAL pour un dump intègre
|
||||
$this->db->exec('PRAGMA wal_checkpoint(FULL);');
|
||||
// Snapshot 100% atomique de la base NexusPBX via VACUUM INTO / .backup officiel SQLite
|
||||
$tmpDbDump = '/tmp/nexus_cluster_db_' . time() . '.sqlite';
|
||||
@unlink($tmpDbDump);
|
||||
try {
|
||||
$this->db->exec("VACUUM INTO " . $this->db->quote($tmpDbDump));
|
||||
} catch (\Throwable $e) {
|
||||
shell_exec("sqlite3 " . escapeshellarg($dbPath) . " \".backup " . escapeshellarg($tmpDbDump) . "\" 2>/dev/null || true");
|
||||
}
|
||||
|
||||
if (!file_exists($tmpDbDump) || filesize($tmpDbDump) === 0) {
|
||||
$this->db->exec('PRAGMA wal_checkpoint(FULL);');
|
||||
@copy($dbPath, $tmpDbDump);
|
||||
}
|
||||
|
||||
$dbIntegrity = trim(shell_exec("sqlite3 " . escapeshellarg($tmpDbDump) . " 'PRAGMA integrity_check;' 2>/dev/null") ?? '');
|
||||
if ($dbIntegrity !== 'ok') {
|
||||
@unlink($tmpDbDump);
|
||||
throw new RuntimeException("Échec de vérification d'intégrité de la base locale avant réplication : {$dbIntegrity}");
|
||||
}
|
||||
|
||||
$tmpArchive = '/tmp/nexus_cluster_sync_' . time() . '.tar.gz';
|
||||
$tmpAstdb = '/tmp/astdb_cluster_sync.sqlite3';
|
||||
@@ -485,7 +502,12 @@ class ClusterManager
|
||||
}
|
||||
}
|
||||
|
||||
$cmd = "tar -czf " . escapeshellarg($tmpArchive) . " -C /var/www/nexus-pbx/storage database.sqlite";
|
||||
// Préparation du dossier temporaire de tarage pour isoler database.sqlite
|
||||
$tmpDbDir = '/tmp/nexus_cluster_tar_' . time();
|
||||
@mkdir($tmpDbDir, 0755, true);
|
||||
rename($tmpDbDump, $tmpDbDir . '/database.sqlite');
|
||||
|
||||
$cmd = "tar -czf " . escapeshellarg($tmpArchive) . " -C " . escapeshellarg($tmpDbDir) . " database.sqlite";
|
||||
if (is_dir('/var/lib/asterisk/sounds/custom')) {
|
||||
$cmd .= " -C /var/lib/asterisk/sounds custom";
|
||||
}
|
||||
@@ -493,6 +515,9 @@ class ClusterManager
|
||||
$cmd .= " -C /tmp astdb_cluster_sync.sqlite3";
|
||||
}
|
||||
shell_exec($cmd);
|
||||
|
||||
@unlink($tmpDbDir . '/database.sqlite');
|
||||
@rmdir($tmpDbDir);
|
||||
if ($hasAstdb) {
|
||||
@unlink($tmpAstdb);
|
||||
}
|
||||
@@ -605,10 +630,31 @@ class ClusterManager
|
||||
$tmpArchive = '/tmp/nexus_cluster_recv_' . time() . '.tar.gz';
|
||||
file_put_contents($tmpArchive, $archiveRaw);
|
||||
|
||||
// Sauvegarde d'urgence de la base actuelle avant écrasement
|
||||
// Extraction sécurisée dans un dossier temporaire pour validation préalable
|
||||
$tmpExtractDir = '/tmp/nexus_cluster_extract_' . time();
|
||||
@mkdir($tmpExtractDir, 0755, true);
|
||||
shell_exec("tar -xzf " . escapeshellarg($tmpArchive) . " -C " . escapeshellarg($tmpExtractDir) . " 2>/dev/null || true");
|
||||
|
||||
$extractedDb = $tmpExtractDir . '/database.sqlite';
|
||||
if (!file_exists($extractedDb) || filesize($extractedDb) === 0) {
|
||||
@unlink($tmpArchive);
|
||||
@rmdir($tmpExtractDir);
|
||||
throw new RuntimeException("L'archive de réplication reçue ne contient pas de base de données valide.");
|
||||
}
|
||||
|
||||
// Contrôle d'intégrité SQLite strict du fichier reçu AVANT toute modification du système local
|
||||
$check = trim(shell_exec("sqlite3 " . escapeshellarg($extractedDb) . " 'PRAGMA integrity_check;' 2>/dev/null") ?? '');
|
||||
if ($check !== 'ok') {
|
||||
@unlink($extractedDb);
|
||||
@rmdir($tmpExtractDir);
|
||||
@unlink($tmpArchive);
|
||||
throw new RuntimeException("La base de données reçue est corrompue ({$check}) : rejet strict de l'application.");
|
||||
}
|
||||
|
||||
// Sauvegarde d'urgence de la base actuelle avant remplacement
|
||||
$dbPath = Database::getDatabasePath();
|
||||
if (file_exists($dbPath)) {
|
||||
copy($dbPath, $dbPath . '.pre_cluster_sync.bak');
|
||||
@copy($dbPath, $dbPath . '.pre_cluster_sync.bak');
|
||||
}
|
||||
|
||||
// Sauvegarder la configuration locale cluster et réseau pour ne pas l'écraser par celle du master
|
||||
@@ -618,26 +664,34 @@ class ClusterManager
|
||||
$localSettingsRow = $this->db->query("SELECT local_net, local_networks_json, admin_https_port FROM settings WHERE id = 1")->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (\Throwable $e) {}
|
||||
|
||||
// Libérer les verrous PDO SQLite
|
||||
// Libérer les verrous PDO SQLite locaux et purger les résidus WAL/SHM pour éviter tout malformed
|
||||
Database::resetConnection();
|
||||
@unlink($dbPath . '-wal');
|
||||
@unlink($dbPath . '-shm');
|
||||
|
||||
// Extraction de l'archive
|
||||
shell_exec("tar -xzf " . escapeshellarg($tmpArchive) . " -C /var/www/nexus-pbx/storage database.sqlite 2>/dev/null || true");
|
||||
// Remplacement atomique de la base de données
|
||||
rename($extractedDb, $dbPath);
|
||||
@chmod($dbPath, 0660);
|
||||
@chown($dbPath, 'www-data');
|
||||
@chgrp($dbPath, 'asterisk');
|
||||
|
||||
// Extraction des sons personnalisés si présents
|
||||
if (is_dir('/var/lib/asterisk/sounds')) {
|
||||
shell_exec("tar -xzf " . escapeshellarg($tmpArchive) . " -C /var/lib/asterisk/sounds custom 2>/dev/null || true");
|
||||
}
|
||||
// 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')) {
|
||||
// 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');
|
||||
|
||||
// Restauration de l'astdb (contacts et IP actives des téléphones PJSIP) via le helper root
|
||||
$extractedAstdb = $tmpExtractDir . '/astdb_cluster_sync.sqlite3';
|
||||
if (file_exists($extractedAstdb)) {
|
||||
shell_exec('sudo -n ' . escapeshellcmd(self::CLUSTER_HELPER_PATH) . ' restore-astdb ' . escapeshellarg($extractedAstdb) . ' 2>&1');
|
||||
@unlink($extractedAstdb);
|
||||
}
|
||||
|
||||
@rmdir($tmpExtractDir);
|
||||
@unlink($tmpArchive);
|
||||
|
||||
// Réouverture de la connexion DB
|
||||
$newDb = Database::getConnection();
|
||||
// Réouverture de la connexion DB fraîche
|
||||
$newDb = Database::getConnection(true);
|
||||
$this->db = $newDb;
|
||||
|
||||
// Restauration de l'identité cluster et réseau propre à CE serveur
|
||||
@@ -686,6 +740,19 @@ class ClusterManager
|
||||
// Régénérer les configurations PJSIP et Asterisk
|
||||
ConfigGenerator::generateAndApply($newDb);
|
||||
|
||||
// Rechargement à chaud d'Asterisk pour appliquer immédiatement la configuration en RAM
|
||||
try {
|
||||
$ami = AmiClient::fromDatabase($newDb);
|
||||
$ami->reloadPjsipAndDialplan();
|
||||
} catch (\Throwable $e) {
|
||||
shell_exec('/usr/sbin/asterisk -rx "dialplan reload" 2>/dev/null; /usr/sbin/asterisk -rx "pjsip reload" 2>/dev/null || true');
|
||||
}
|
||||
|
||||
// En mode standby / veille, s'assurer que les Trunks externes ne s'enregistrent pas pour éviter les conflits
|
||||
if (($localClusterSettings['role'] ?? 'backup') !== 'master') {
|
||||
shell_exec('/usr/sbin/asterisk -rx "pjsip send unregister *" 2>/dev/null || true');
|
||||
}
|
||||
|
||||
// Si le cluster est actif, actualiser le service Keepalived
|
||||
if (!empty($localClusterSettings['enabled'])) {
|
||||
$this->applyKeepalivedConfig();
|
||||
|
||||
Reference in new issue
Block a user