fix(cluster): support custom peer port, fix netmask & vrid persistence, and fix static call

This commit is contained in:
sorcier committed 2026-11-05 19:35:10 +01:00
1 parent da0805fd5f
commit b76490a4dc
4 files changed
+82 -19

No files matched your search

+10
View File
@@ -6376,12 +6376,21 @@ async function refreshClusterStatus() {
const vipInput = document.getElementById('cluster-virtual-ip'); const vipInput = document.getElementById('cluster-virtual-ip');
if (vipInput) vipInput.value = st.virtual_ip || ''; if (vipInput) vipInput.value = st.virtual_ip || '';
const vmaskInput = document.getElementById('cluster-virtual-netmask');
if (vmaskInput && st.virtual_netmask) vmaskInput.value = st.virtual_netmask;
const vridInput = document.getElementById('cluster-vrid'); const vridInput = document.getElementById('cluster-vrid');
if (vridInput && st.vrid) vridInput.value = st.vrid; if (vridInput && st.vrid) vridInput.value = st.vrid;
const peerIpInput = document.getElementById('cluster-peer-ip'); const peerIpInput = document.getElementById('cluster-peer-ip');
if (peerIpInput) peerIpInput.value = st.peer_ip || ''; if (peerIpInput) peerIpInput.value = st.peer_ip || '';
const peerPortInput = document.getElementById('cluster-peer-port');
if (peerPortInput && st.peer_port) peerPortInput.value = st.peer_port;
const autoSyncChk = document.getElementById('cluster-auto-sync');
if (autoSyncChk && st.auto_sync !== undefined) autoSyncChk.checked = Boolean(st.auto_sync);
const secretInput = document.getElementById('cluster-secret'); const secretInput = document.getElementById('cluster-secret');
if (secretInput && st.cluster_secret) secretInput.value = st.cluster_secret; if (secretInput && st.cluster_secret) secretInput.value = st.cluster_secret;
@@ -6572,6 +6581,7 @@ async function saveClusterSettings(e) {
virtual_ip: form.elements['virtual_ip'].value.trim(), virtual_ip: form.elements['virtual_ip'].value.trim(),
virtual_netmask: form.elements['virtual_netmask'].value.trim() || '24', virtual_netmask: form.elements['virtual_netmask'].value.trim() || '24',
peer_ip: form.elements['peer_ip'].value.trim(), peer_ip: form.elements['peer_ip'].value.trim(),
peer_port: parseInt(form.elements['peer_port'] ? form.elements['peer_port'].value : 8443, 10) || 8443,
cluster_secret: form.elements['cluster_secret'].value.trim(), cluster_secret: form.elements['cluster_secret'].value.trim(),
vrid: parseInt(form.elements['vrid'].value, 10) || 51, vrid: parseInt(form.elements['vrid'].value, 10) || 51,
auto_sync: form.elements['auto_sync'].checked ? 1 : 0 auto_sync: form.elements['auto_sync'].checked ? 1 : 0
+7 -2
View File
@@ -2917,9 +2917,14 @@ $isDevEnv = file_exists(__DIR__ . '/../.dev')
<!-- IP DU PAIR & JETON SECRET --> <!-- IP DU PAIR & JETON SECRET -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div> <div>
<label class="block text-xs font-bold text-slate-300 uppercase mb-1">IP du Nœud Pair (Peer IP)</label> <label class="block text-xs font-bold text-slate-300 uppercase mb-1">IP & Port d'Administration du Pair</label>
<div class="flex gap-2">
<input type="text" id="cluster-peer-ip" name="peer_ip" placeholder="ex: 10.10.0.5" <input type="text" id="cluster-peer-ip" name="peer_ip" placeholder="ex: 10.10.0.5"
class="w-full px-3 py-2 rounded-xl bg-anthracite-900 border border-anthracite-700 text-sm text-white focus:outline-none focus:border-cyan-400 font-mono"> class="flex-1 px-3 py-2 rounded-xl bg-anthracite-900 border border-anthracite-700 text-sm text-white focus:outline-none focus:border-cyan-400 font-mono">
<span class="flex items-center text-slate-400 font-mono text-sm">:</span>
<input type="number" id="cluster-peer-port" name="peer_port" value="8443" min="1" max="65535" placeholder="8443"
class="w-24 px-3 py-2 rounded-xl bg-anthracite-900 border border-anthracite-700 text-sm text-white text-center focus:outline-none focus:border-cyan-400 font-mono" title="Port HTTPS d'administration du serveur distant (ex: 8443 ou 2356)">
</div>
</div> </div>
<div> <div>
<label class="block text-xs font-bold text-slate-300 uppercase mb-1">Jeton de Sécurité Cluster (Secret)</label> <label class="block text-xs font-bold text-slate-300 uppercase mb-1">Jeton de Sécurité Cluster (Secret)</label>
+58 -17
View File
@@ -38,6 +38,7 @@ class ClusterManager
'virtual_netmask' => '24', 'virtual_netmask' => '24',
'interface' => 'eth0', 'interface' => 'eth0',
'peer_ip' => '', 'peer_ip' => '',
'peer_port' => 8443,
'cluster_secret' => bin2hex(random_bytes(16)), 'cluster_secret' => bin2hex(random_bytes(16)),
'vrid' => 51, 'vrid' => 51,
'priority' => 100, 'priority' => 100,
@@ -48,6 +49,9 @@ class ClusterManager
'last_sync_message' => '', 'last_sync_message' => '',
]; ];
} }
if (empty($row['peer_port'])) {
$row['peer_port'] = 8443;
}
return $row; return $row;
} }
@@ -61,13 +65,35 @@ class ClusterManager
$virtualIp = trim((string)($data['virtual_ip'] ?? '')); $virtualIp = trim((string)($data['virtual_ip'] ?? ''));
$virtualNetmask = preg_replace('/[^0-9]/', '', (string)($data['virtual_netmask'] ?? '24')) ?: '24'; $virtualNetmask = preg_replace('/[^0-9]/', '', (string)($data['virtual_netmask'] ?? '24')) ?: '24';
$interface = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', trim((string)($data['interface'] ?? 'eth0'))) ?: 'eth0'; $interface = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', trim((string)($data['interface'] ?? 'eth0'))) ?: 'eth0';
$peerIp = trim((string)($data['peer_ip'] ?? '')); $peerIpRaw = trim((string)($data['peer_ip'] ?? ''));
$peerPort = (int)($data['peer_port'] ?? 0);
$clusterSecret = trim((string)($data['cluster_secret'] ?? '')); $clusterSecret = trim((string)($data['cluster_secret'] ?? ''));
$vrid = max(1, min(255, (int)($data['vrid'] ?? 51))); $vrid = max(1, min(255, (int)($data['vrid'] ?? 51)));
$priority = max(1, min(254, (int)($data['priority'] ?? ($role === 'master' ? 100 : 90)))); $priority = max(1, min(254, (int)($data['priority'] ?? ($role === 'master' ? 100 : 90))));
$syncInterval = max(5, min(3600, (int)($data['sync_interval'] ?? 15))); $syncInterval = max(5, min(3600, (int)($data['sync_interval'] ?? 15)));
$autoSync = !empty($data['auto_sync']) ? 1 : 0; $autoSync = !empty($data['auto_sync']) ? 1 : 0;
// Détection et parsing si le port est saisi dans l'IP (ex: 10.10.11.197:2356)
if (str_contains($peerIpRaw, ':')) {
$parts = explode(':', $peerIpRaw, 2);
$peerIp = trim($parts[0]);
$parsedPort = (int)trim($parts[1]);
if ($parsedPort > 0 && $parsedPort <= 65535) {
$peerPort = $parsedPort;
}
} else {
$peerIp = $peerIpRaw;
}
if ($peerPort <= 0 || $peerPort > 65535) {
try {
$st = $this->db->query("SELECT admin_https_port FROM settings WHERE id = 1")->fetch(PDO::FETCH_ASSOC);
$peerPort = (int)($st['admin_https_port'] ?? 8443);
} catch (\Throwable $e) {
$peerPort = 8443;
}
}
if ($clusterSecret === '') { if ($clusterSecret === '') {
$clusterSecret = bin2hex(random_bytes(16)); $clusterSecret = bin2hex(random_bytes(16));
} }
@@ -100,6 +126,7 @@ class ClusterManager
virtual_netmask = :vmask, virtual_netmask = :vmask,
interface = :iface, interface = :iface,
peer_ip = :peer, peer_ip = :peer,
peer_port = :peer_port,
cluster_secret = :secret, cluster_secret = :secret,
vrid = :vrid, vrid = :vrid,
priority = :prio, priority = :prio,
@@ -116,6 +143,7 @@ class ClusterManager
':vmask' => $virtualNetmask, ':vmask' => $virtualNetmask,
':iface' => $interface, ':iface' => $interface,
':peer' => $peerIp, ':peer' => $peerIp,
':peer_port' => $peerPort,
':secret' => $clusterSecret, ':secret' => $clusterSecret,
':vrid' => $vrid, ':vrid' => $vrid,
':prio' => $priority, ':prio' => $priority,
@@ -253,6 +281,11 @@ class ClusterManager
'configured_role' => $settings['role'], 'configured_role' => $settings['role'],
'effective_state' => $effectiveState, 'effective_state' => $effectiveState,
'virtual_ip' => $vip, 'virtual_ip' => $vip,
'virtual_netmask' => $settings['virtual_netmask'] ?? '24',
'vrid' => (int)($settings['vrid'] ?? 51),
'priority' => (int)($settings['priority'] ?? 100),
'peer_port' => (int)($settings['peer_port'] ?? 8443),
'auto_sync' => (bool)($settings['auto_sync'] ?? 1),
'virtual_ip_active' => $isVipActive, 'virtual_ip_active' => $isVipActive,
'interface' => $settings['interface'], 'interface' => $settings['interface'],
'peer_ip' => $peerIp, 'peer_ip' => $peerIp,
@@ -402,22 +435,24 @@ class ClusterManager
{ {
$settings = $this->getSettings(); $settings = $this->getSettings();
$peerIp = trim((string)($settings['peer_ip'] ?? '')); $peerIp = trim((string)($settings['peer_ip'] ?? ''));
$peerPort = (int)($settings['peer_port'] ?? 8443);
if ($peerPort <= 0 || $peerPort > 65535) {
$peerPort = 8443;
}
$secret = trim((string)($settings['cluster_secret'] ?? '')); $secret = trim((string)($settings['cluster_secret'] ?? ''));
if ($peerIp === '') { if ($peerIp === '') {
throw new RuntimeException("L'adresse IP du Nœud pair (Peer IP) n'est pas configurée."); throw new RuntimeException("L'adresse IP du Nœud pair (Peer IP) n'est pas configurée.");
} }
// Port d'administration HTTPS configuré // Port d'administration HTTPS local configuré pour éventuel fallback
$adminPort = 8443; $localAdminPort = 8443;
try { try {
$st = $this->db->query("SELECT admin_https_port FROM settings WHERE id = 1")->fetch(PDO::FETCH_ASSOC); $st = $this->db->query("SELECT admin_https_port FROM settings WHERE id = 1")->fetch(PDO::FETCH_ASSOC);
if (!empty($st['admin_https_port'])) { if (!empty($st['admin_https_port'])) {
$adminPort = (int)$st['admin_https_port']; $localAdminPort = (int)$st['admin_https_port'];
}
} catch (\Throwable $e) {
$adminPort = 8443;
} }
} catch (\Throwable $e) {}
// Création de l'archive de réplication // Création de l'archive de réplication
$dbPath = Database::getDatabasePath(); $dbPath = Database::getDatabasePath();
@@ -451,16 +486,19 @@ class ClusterManager
$jsonPayload = json_encode($payload); $jsonPayload = json_encode($payload);
$signature = hash_hmac('sha256', $jsonPayload, $secret); $signature = hash_hmac('sha256', $jsonPayload, $secret);
// URLs d'accès candidates vers l'API du second nœud // URLs d'accès candidates vers l'API du second nœud (priorité au port configuré)
$candidateUrls = [ $candidateUrls = [
"https://{$peerIp}:{$adminPort}/api.php?action=cluster_receive_sync", "https://{$peerIp}:{$peerPort}/api.php?action=cluster_receive_sync",
]; ];
if ($adminPort !== 443) { if ($peerPort !== $localAdminPort) {
$candidateUrls[] = "https://{$peerIp}/api.php?action=cluster_receive_sync"; $candidateUrls[] = "https://{$peerIp}:{$localAdminPort}/api.php?action=cluster_receive_sync";
} }
if ($adminPort !== 8443) { if ($peerPort !== 8443 && $localAdminPort !== 8443) {
$candidateUrls[] = "https://{$peerIp}:8443/api.php?action=cluster_receive_sync"; $candidateUrls[] = "https://{$peerIp}:8443/api.php?action=cluster_receive_sync";
} }
if ($peerPort !== 443 && $localAdminPort !== 443) {
$candidateUrls[] = "https://{$peerIp}/api.php?action=cluster_receive_sync";
}
$candidateUrls[] = "http://{$peerIp}/api.php?action=cluster_receive_sync"; $candidateUrls[] = "http://{$peerIp}/api.php?action=cluster_receive_sync";
$candidateUrls = array_values(array_unique($candidateUrls)); $candidateUrls = array_values(array_unique($candidateUrls));
@@ -494,7 +532,9 @@ class ClusterManager
} }
$lastError = "Rejet par le pair ({$targetUrl}) : " . ($resJson['message'] ?? $resJson['error'] ?? 'Erreur inconnue'); $lastError = "Rejet par le pair ({$targetUrl}) : " . ($resJson['message'] ?? $resJson['error'] ?? 'Erreur inconnue');
} else { } else {
$lastError = "Échec d'envoi vers {$targetUrl} (HTTP {$httpCode})" . ($err ? " : {$err}" : ""); $resJson = json_decode((string)$response, true);
$detail = !empty($resJson['message']) ? " - " . $resJson['message'] : ($err ? " : {$err}" : "");
$lastError = "Échec d'envoi vers {$targetUrl} (HTTP {$httpCode}){$detail}";
} }
} }
@@ -503,11 +543,11 @@ class ClusterManager
throw new RuntimeException($lastError); throw new RuntimeException($lastError);
} }
$this->updateSyncStatus('success', "Synchronisation réussie avec le nœud {$peerIp}"); $this->updateSyncStatus('success', "Synchronisation réussie avec le nœud {$peerIp}:{$peerPort}");
return [ return [
'success' => true, 'success' => true,
'message' => "Réplication transmise et appliquée sur le nœud pair {$peerIp} avec succès.", 'message' => "Réplication transmise et appliquée sur le nœud pair {$peerIp}:{$peerPort} avec succès.",
]; ];
} }
@@ -571,6 +611,7 @@ class ClusterManager
role = :role, role = :role,
interface = :iface, interface = :iface,
peer_ip = :peer_ip, peer_ip = :peer_ip,
peer_port = :peer_port,
priority = :priority, priority = :priority,
virtual_ip = :virtual_ip, virtual_ip = :virtual_ip,
virtual_netmask = :virtual_netmask, virtual_netmask = :virtual_netmask,
@@ -584,6 +625,7 @@ class ClusterManager
':role' => $localClusterSettings['role'] ?? 'backup', ':role' => $localClusterSettings['role'] ?? 'backup',
':iface' => $localClusterSettings['interface'] ?? 'eth0', ':iface' => $localClusterSettings['interface'] ?? 'eth0',
':peer_ip' => $localClusterSettings['peer_ip'] ?? '', ':peer_ip' => $localClusterSettings['peer_ip'] ?? '',
':peer_port' => (int)($localClusterSettings['peer_port'] ?? 8443),
':priority' => $localClusterSettings['priority'] ?? 90, ':priority' => $localClusterSettings['priority'] ?? 90,
':virtual_ip' => $localClusterSettings['virtual_ip'] ?? '', ':virtual_ip' => $localClusterSettings['virtual_ip'] ?? '',
':virtual_netmask' => $localClusterSettings['virtual_netmask'] ?? '24', ':virtual_netmask' => $localClusterSettings['virtual_netmask'] ?? '24',
@@ -607,8 +649,7 @@ class ClusterManager
} }
// Régénérer les configurations PJSIP et Asterisk // Régénérer les configurations PJSIP et Asterisk
$generator = new ConfigGenerator($newDb); ConfigGenerator::generateAndApply($newDb);
$generator->generateAndApply();
// Si le cluster est actif, actualiser le service Keepalived // Si le cluster est actif, actualiser le service Keepalived
if (!empty($localClusterSettings['enabled'])) { if (!empty($localClusterSettings['enabled'])) {
+7
View File
@@ -317,6 +317,7 @@ class Database
virtual_netmask VARCHAR(16) NOT NULL DEFAULT '24', virtual_netmask VARCHAR(16) NOT NULL DEFAULT '24',
interface VARCHAR(32) NOT NULL DEFAULT 'eth0', interface VARCHAR(32) NOT NULL DEFAULT 'eth0',
peer_ip VARCHAR(45) NOT NULL DEFAULT '', peer_ip VARCHAR(45) NOT NULL DEFAULT '',
peer_port INTEGER NOT NULL DEFAULT 8443,
cluster_secret VARCHAR(64) NOT NULL DEFAULT '', cluster_secret VARCHAR(64) NOT NULL DEFAULT '',
vrid INTEGER NOT NULL DEFAULT 51, vrid INTEGER NOT NULL DEFAULT 51,
priority INTEGER NOT NULL DEFAULT 100, priority INTEGER NOT NULL DEFAULT 100,
@@ -784,6 +785,7 @@ class Database
virtual_netmask VARCHAR(16) NOT NULL DEFAULT '24', virtual_netmask VARCHAR(16) NOT NULL DEFAULT '24',
interface VARCHAR(32) NOT NULL DEFAULT 'eth0', interface VARCHAR(32) NOT NULL DEFAULT 'eth0',
peer_ip VARCHAR(45) NOT NULL DEFAULT '', peer_ip VARCHAR(45) NOT NULL DEFAULT '',
peer_port INTEGER NOT NULL DEFAULT 8443,
cluster_secret VARCHAR(64) NOT NULL DEFAULT '', cluster_secret VARCHAR(64) NOT NULL DEFAULT '',
vrid INTEGER NOT NULL DEFAULT 51, vrid INTEGER NOT NULL DEFAULT 51,
priority INTEGER NOT NULL DEFAULT 100, priority INTEGER NOT NULL DEFAULT 100,
@@ -797,6 +799,11 @@ class Database
); );
"); ");
// Migration de la colonne peer_port si elle n'existe pas encore
try {
$db->exec("ALTER TABLE cluster_settings ADD COLUMN peer_port INTEGER NOT NULL DEFAULT 8443");
} catch (\Throwable $e) {}
$count = (int)$db->query('SELECT COUNT(*) FROM cluster_settings')->fetchColumn(); $count = (int)$db->query('SELECT COUNT(*) FROM cluster_settings')->fetchColumn();
if ($count === 0) { if ($count === 0) {
$defaultSecret = bin2hex(random_bytes(16)); $defaultSecret = bin2hex(random_bytes(16));