fix(ha): try/finally & purge temp /tmp cluster (v2.5.2)

This commit is contained in:
sorcier committed 2026-11-11 19:48:26 +01:00
1 parent c96af67ac9
commit 8df103d7a0
3 files changed
+62 -32

No files matched your search

+3
View File
@@ -81,6 +81,9 @@ fi
echo -e "\n${C_YELLOW}-> Mise à jour vers la version v${REMOTE_VER}...${C_RESET}"
# Nettoyage préventif d'éventuels anciens dossiers temporaires HA dans /tmp
find /tmp -maxdepth 1 \( -name 'nexus_cluster_extract_*' -o -name 'nexus_cluster_tar_*' -o -name 'nexus_cluster_recv_*' -o -name 'nexus_cluster_sync_*' -o -name 'nexus_cluster_db_*' \) -mmin +10 -exec rm -rf {} + 2>/dev/null || true
# 1. Sauvegarde préventive automatique (Disaster Recovery complète)
echo -e "${C_GRAY}[1/6] Création d'une sauvegarde préventive du système (Disaster Recovery)...${C_RESET}"
php -r '
+57 -31
View File
@@ -495,10 +495,17 @@ class ClusterManager
throw new RuntimeException("Fichier de base de données introuvable : {$dbPath}");
}
$this->cleanupStaleClusterTempFiles();
$uniq = getmypid() . '_' . bin2hex(random_bytes(4));
// Snapshot 100% atomique de la base NexusPBX via VACUUM INTO / .backup officiel SQLite
$tmpDbDump = '/tmp/nexus_cluster_db_' . $uniq . '.sqlite';
$tmpArchive = '/tmp/nexus_cluster_sync_' . $uniq . '.tar.gz';
$tmpAstdb = '/tmp/astdb_cluster_sync_' . $uniq . '.sqlite3';
$tmpDbDir = '/tmp/nexus_cluster_tar_' . $uniq;
$hasAstdb = false;
try {
@unlink($tmpDbDump);
try {
$this->db->exec("VACUUM INTO " . $this->db->quote($tmpDbDump));
@@ -513,14 +520,9 @@ class ClusterManager
$dbIntegrity = $this->checkSqliteIntegrity($tmpDbDump);
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_' . $uniq . '.tar.gz';
$tmpAstdb = '/tmp/astdb_cluster_sync_' . $uniq . '.sqlite3';
$hasAstdb = false;
// Sauvegarde intègre et atomique de la base de contacts Asterisk (astdb.sqlite3)
$astdbPath = '/var/lib/asterisk/astdb.sqlite3';
if (file_exists($astdbPath) && is_readable($astdbPath)) {
@@ -550,14 +552,11 @@ class ClusterManager
shell_exec("sqlite3 " . escapeshellarg($tmpAstdb) . " \"DELETE FROM astdb WHERE key LIKE '/subscription_persistence/%';\" 2>/dev/null || true");
}
$hasAstdb = true;
} else {
@unlink($tmpAstdb);
}
}
}
// Préparation du dossier temporaire de tarage pour isoler database.sqlite
$tmpDbDir = '/tmp/nexus_cluster_tar_' . $uniq;
@mkdir($tmpDbDir, 0755, true);
rename($tmpDbDump, $tmpDbDir . '/database.sqlite');
@@ -576,19 +575,17 @@ class ClusterManager
}
exec($cmd . " 2>&1", $tarOut, $tarCode);
@unlink($tmpDbDir . '/database.sqlite');
@rmdir($tmpDbDir);
if ($hasAstdb) {
@unlink($tmpAstdb);
}
if (!file_exists($tmpArchive) || filesize($tmpArchive) === 0 || $tarCode >= 2) {
@unlink($tmpArchive);
throw new RuntimeException("Échec de génération de l'archive de réplication (tar code {$tarCode}).");
}
$archiveData = file_get_contents($tmpArchive);
} finally {
@unlink($tmpDbDump);
@unlink($tmpAstdb);
@unlink($tmpArchive);
shell_exec('rm -rf ' . escapeshellarg($tmpDbDir) . ' 2>/dev/null || true');
}
// Collecte des adresses IP bannies dans Fail2ban (jails actives)
$bannedIps = $this->getActiveFail2banBans();
@@ -694,28 +691,37 @@ class ClusterManager
throw new RuntimeException("Paquet de réplication vide ou corrompu.");
}
$this->cleanupStaleClusterTempFiles();
$uniqRecv = getmypid() . '_' . bin2hex(random_bytes(4));
$archiveRaw = base64_decode($data['archive_b64']);
$archiveRaw = base64_decode($data['archive_b64'], true);
if ($archiveRaw === false || $archiveRaw === '') {
throw new RuntimeException("Archive de réplication invalide (décodage base64).");
}
$tmpArchive = '/tmp/nexus_cluster_recv_' . $uniqRecv . '.tar.gz';
file_put_contents($tmpArchive, $archiveRaw);
$tmpExtractDir = '/tmp/nexus_cluster_extract_' . $uniqRecv;
$localClusterSettings = $settings;
$localSettingsRow = null;
try {
$written = @file_put_contents($tmpArchive, $archiveRaw);
if ($written === false || $written !== strlen($archiveRaw)) {
throw new RuntimeException("Espace insuffisant dans /tmp lors de l'écriture de l'archive HA.");
}
// Extraction sécurisée dans un dossier temporaire pour validation préalable
$tmpExtractDir = '/tmp/nexus_cluster_extract_' . $uniqRecv;
@mkdir($tmpExtractDir, 0755, true);
shell_exec("tar -xzf " . escapeshellarg($tmpArchive) . " -C " . escapeshellarg($tmpExtractDir) . " 2>/dev/null || true");
exec("tar -xzf " . escapeshellarg($tmpArchive) . " -C " . escapeshellarg($tmpExtractDir) . " 2>&1", $extOut, $extCode);
$extractedDb = $tmpExtractDir . '/database.sqlite';
if (!file_exists($extractedDb) || filesize($extractedDb) === 0) {
@unlink($tmpArchive);
shell_exec('rm -rf ' . escapeshellarg($tmpExtractDir) . ' 2>/dev/null || true');
throw new RuntimeException("L'archive de réplication reçue ne contient pas de base de données valide.");
$detail = !empty($extOut) ? ' (' . implode(' | ', array_slice($extOut, 0, 2)) . ')' : '';
throw new RuntimeException("L'archive de réplication reçue ne contient pas de base de données valide{$detail}.");
}
// Contrôle d'intégrité SQLite strict du fichier reçu AVANT toute modification du système local
$check = $this->checkSqliteIntegrity($extractedDb);
if ($check !== 'ok') {
shell_exec('rm -rf ' . escapeshellarg($tmpExtractDir) . ' 2>/dev/null || true');
@unlink($tmpArchive);
throw new RuntimeException("La base de données reçue est corrompue ({$check}) : rejet strict de l'application.");
}
@@ -726,15 +732,11 @@ class ClusterManager
}
// Sauvegarder la configuration locale cluster et réseau pour ne pas l'écraser par celle du master
$localClusterSettings = $settings;
$localSettingsRow = null;
try {
$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) {}
// Restauration officielle en ligne via l'API SQLite (SQLite3::backup ou sqlite3 .restore)
// Ne détruit aucun descripteur de fichier, ne touche pas aux journaux WAL/SHM,
// et n'invalide aucune lecture concurrente dans PHP-FPM.
if (file_exists($dbPath)) {
$this->restoreSqliteDatabase($extractedDb, $dbPath);
} else {
@@ -752,8 +754,6 @@ class ClusterManager
if (file_exists($dbPath . '.pre_cluster_sync.bak')) {
$this->restoreSqliteDatabase($dbPath . '.pre_cluster_sync.bak', $dbPath);
}
shell_exec('rm -rf ' . escapeshellarg($tmpExtractDir) . ' 2>/dev/null || true');
@unlink($tmpArchive);
throw new RuntimeException("Échec de vérification d'intégrité après synchronisation ({$postCheck}). Restauration préventive effectuée.");
}
@@ -794,9 +794,10 @@ class ClusterManager
shell_exec('sudo -n ' . escapeshellcmd(self::CLUSTER_HELPER_PATH) . ' restore-astdb ' . escapeshellarg($extractedAstdb) . ' 2>&1');
@unlink($extractedAstdb);
}
} finally {
shell_exec('rm -rf ' . escapeshellarg($tmpExtractDir) . ' 2>/dev/null || true');
@unlink($tmpArchive);
}
// Réouverture de la connexion DB fraîche
$newDb = Database::getConnection(true);
@@ -1119,6 +1120,31 @@ class ClusterManager
];
}
private function cleanupStaleClusterTempFiles(): void
{
$cutoff = time() - 600;
$patterns = [
'/tmp/nexus_cluster_extract_*',
'/tmp/nexus_cluster_tar_*',
'/tmp/nexus_cluster_recv_*',
'/tmp/nexus_cluster_sync_*',
'/tmp/nexus_cluster_db_*',
'/tmp/astdb_cluster_sync*',
];
foreach ($patterns as $pattern) {
foreach (glob($pattern) ?: [] as $path) {
$mtime = @filemtime($path);
if ($mtime !== false && $mtime < $cutoff) {
if (is_dir($path)) {
shell_exec('rm -rf ' . escapeshellarg($path) . ' 2>/dev/null || true');
} else {
@unlink($path);
}
}
}
}
}
private function checkSqliteIntegrity(string $filePath): string
{
if (!file_exists($filePath) || filesize($filePath) === 0) {
+2 -1
View File
@@ -1,9 +1,10 @@
{
"version": "2.5.1",
"version": "2.5.2",
"codename": "Pulsar UCP",
"release_date": "2026-11-11",
"min_php_version": "8.2",
"changelog": [
"Correctif v2.5.2 : purge systématique try/finally + auto-nettoyage des dossiers temporaires HA /tmp (élimine la saturation tmpfs 100% qui bloquait la réplication)",
"Correctif v2.5.1 : auto-déploiement automatique Nginx/Fail2ban UCP même après un git pull manuel",
"Correctif v2.5.1 : vérification d'intégrité & restauration SQLite HA via PDO / SQLite3::backup sans dépendance CLI sqlite3 et anti-collision /tmp",
"Correctif v2.5.1 : rafraîchissement instantané de la liste des utilisateurs UCP à la création/suppression sans rechargement manuel de la page",