feat: gestion réseau DHCP/statique depuis l'interface web

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jules
2026-05-06 01:23:38 +02:00
parent 29da0a8380
commit 9b7cf851c2
3 changed files with 296 additions and 0 deletions

41
scripts/apply_network.sh Normal file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
# apply_network.sh — Configure le réseau via NetworkManager
# Usage: apply_network.sh dhcp
# ou: apply_network.sh static <ip/prefix> <gateway> <dns>
# Exemple: apply_network.sh static 192.168.1.100/24 192.168.1.1 8.8.8.8,1.1.1.1
set -e
MODE="$1"
CON="Wired connection 1"
# Trouve la connexion active si le nom par défaut a changé
if ! nmcli con show "$CON" &>/dev/null; then
CON=$(nmcli -t -f NAME,TYPE con show --active | grep ethernet | cut -d: -f1 | head -1)
fi
[[ -z "$CON" ]] && echo "Aucune connexion Ethernet trouvée." && exit 1
if [[ "$MODE" == "dhcp" ]]; then
nmcli con mod "$CON" ipv4.method auto ipv4.addresses "" ipv4.gateway "" ipv4.dns ""
nmcli con up "$CON"
echo "OK: mode DHCP appliqué."
elif [[ "$MODE" == "static" ]]; then
IP_PREFIX="$2" # ex: 192.168.1.100/24
GATEWAY="$3" # ex: 192.168.1.1
DNS="$4" # ex: 8.8.8.8,1.1.1.1
[[ -z "$IP_PREFIX" || -z "$GATEWAY" ]] && echo "IP et gateway requis." && exit 1
nmcli con mod "$CON" \
ipv4.method manual \
ipv4.addresses "$IP_PREFIX" \
ipv4.gateway "$GATEWAY" \
ipv4.dns "${DNS:-8.8.8.8,1.1.1.1}"
nmcli con up "$CON"
echo "OK: IP statique $IP_PREFIX appliquée."
else
echo "Usage: $0 dhcp | static <ip/prefix> <gateway> [dns]" && exit 1
fi

View File

@@ -118,5 +118,169 @@ html_navbar('domain');
<a href="/admin/index.php" class="btn btn-outline-secondary">Annuler</a> <a href="/admin/index.php" class="btn btn-outline-secondary">Annuler</a>
</div> </div>
</form> </form>
<!-- ── Réseau (hors formulaire principal) ── -->
<div class="card shadow-sm mt-4">
<div class="card-header fw-semibold d-flex justify-content-between align-items-center">
<span><i class="bi bi-hdd-network me-2"></i>Réseau</span>
<span id="net-current-ip" class="badge bg-secondary font-monospace">chargement…</span>
</div>
<div class="card-body">
<div id="net-loading" class="text-muted small mb-3">
<div class="spinner-border spinner-border-sm me-2"></div>Lecture de la configuration…
</div>
<div id="net-form" style="display:none">
<!-- Mode -->
<div class="mb-3">
<label class="form-label fw-semibold">Mode</label>
<div class="d-flex gap-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="net-mode" id="net-dhcp" value="dhcp">
<label class="form-check-label" for="net-dhcp">DHCP <span class="text-muted small">(automatique)</span></label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="net-mode" id="net-static" value="static">
<label class="form-check-label" for="net-static">IP Statique</label>
</div>
</div>
</div>
<!-- Champs IP statique -->
<div id="net-static-fields" style="display:none">
<div class="row g-3 mb-3">
<div class="col-md-5">
<label class="form-label">Adresse IP</label>
<input type="text" id="net-ip" class="form-control font-monospace" placeholder="192.168.1.100">
</div>
<div class="col-md-2">
<label class="form-label">Masque</label>
<select id="net-prefix" class="form-select font-monospace">
<option value="24">/24 — 255.255.255.0</option>
<option value="25">/25 — 255.255.255.128</option>
<option value="16">/16 — 255.255.0.0</option>
<option value="8">/8 — 255.0.0.0</option>
</select>
</div>
<div class="col-md-5">
<label class="form-label">Passerelle</label>
<input type="text" id="net-gw" class="form-control font-monospace" placeholder="192.168.1.1">
</div>
<div class="col-md-6">
<label class="form-label">Serveurs DNS <span class="text-muted small">(séparés par virgule)</span></label>
<input type="text" id="net-dns" class="form-control font-monospace" placeholder="8.8.8.8,1.1.1.1">
</div>
</div>
</div>
<div id="net-alert" class="alert py-2 mb-3" style="display:none"></div>
<div class="d-flex gap-2 align-items-center">
<button class="btn btn-warning" onclick="applyNetwork()">
<i class="bi bi-hdd-network me-1"></i>Appliquer la configuration réseau
</button>
<span id="net-spinner" class="spinner-border spinner-border-sm text-warning" style="display:none"></span>
<span class="text-muted small" id="net-warn" style="display:none">
<i class="bi bi-exclamation-triangle me-1 text-warning"></i>
Si vous changez l'IP, reconnectez-vous sur la nouvelle adresse.
</span>
</div>
</div>
</div>
</div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', function() {
loadNetworkConfig();
document.querySelectorAll('input[name="net-mode"]').forEach(function(r) {
r.addEventListener('change', function() {
document.getElementById('net-static-fields').style.display = r.value === 'static' ? '' : 'none';
document.getElementById('net-warn').style.display = r.value === 'static' ? '' : 'none';
});
});
});
function loadNetworkConfig() {
fetch('/api/network.php')
.then(r => r.json())
.then(function(d) {
document.getElementById('net-loading').style.display = 'none';
document.getElementById('net-form').style.display = '';
// IP actuelle
const ip = d.current_ip || d.static_ip || '—';
document.getElementById('net-current-ip').textContent = ip;
// Mode
const mode = d.method || 'dhcp';
document.querySelector('input[name="net-mode"][value="' + mode + '"]').checked = true;
// Champs statique
if (mode === 'static') {
document.getElementById('net-static-fields').style.display = '';
document.getElementById('net-warn').style.display = '';
}
// Pré-remplissage IP statique
const sipRaw = d.static_ip || d.current_ip || '';
const sipParts = sipRaw.split('/');
document.getElementById('net-ip').value = sipParts[0] || '';
document.getElementById('net-prefix').value = sipParts[1] || '24';
document.getElementById('net-gw').value = d.static_gw || d.current_gw || '';
document.getElementById('net-dns').value = (d.static_dns || d.current_dns || '8.8.8.8,1.1.1.1').replace(/\s/g,'');
})
.catch(function() {
document.getElementById('net-loading').innerHTML = '<span class="text-danger"><i class="bi bi-exclamation-triangle me-1"></i>Impossible de lire la configuration réseau.</span>';
});
}
function applyNetwork() {
const mode = document.querySelector('input[name="net-mode"]:checked')?.value || 'dhcp';
const body = { mode };
if (mode === 'static') {
body.ip = document.getElementById('net-ip').value.trim();
body.prefix = document.getElementById('net-prefix').value;
body.gateway = document.getElementById('net-gw').value.trim();
body.dns = document.getElementById('net-dns').value.trim();
if (!body.ip || !body.gateway) {
showNetAlert('danger', 'Veuillez remplir l\'adresse IP et la passerelle.');
return;
}
}
document.getElementById('net-spinner').style.display = '';
document.getElementById('net-alert').style.display = 'none';
fetch('/api/network.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body)
})
.then(r => r.json())
.then(function(d) {
document.getElementById('net-spinner').style.display = 'none';
if (d.success) {
let msg = d.msg || 'Configuration appliquée.';
if (d.new_ip) msg += ' — Reconnectez-vous sur <strong>' + d.new_ip + '</strong>';
showNetAlert('success', msg);
setTimeout(loadNetworkConfig, 2000);
} else {
showNetAlert('danger', d.error || 'Erreur inconnue.');
}
})
.catch(function() {
document.getElementById('net-spinner').style.display = 'none';
showNetAlert('warning', 'Pas de réponse — l\'IP a peut-être changé. Reconnectez-vous.');
});
}
function showNetAlert(type, msg) {
const el = document.getElementById('net-alert');
el.className = 'alert alert-' + type + ' py-2 mb-3';
el.innerHTML = msg;
el.style.display = '';
}
</script>
<?php html_foot(); ?> <?php html_foot(); ?>

91
web/api/network.php Normal file
View File

@@ -0,0 +1,91 @@
<?php
require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/../includes/auth.php';
admin_check();
header('Content-Type: application/json');
// Lit la config réseau actuelle via nmcli
function get_network_info(): array {
// Connexion active
$conName = trim(shell_exec("nmcli -t -f NAME,TYPE con show --active 2>/dev/null | grep ethernet | cut -d: -f1 | head -1") ?? '');
if (!$conName) $conName = 'Wired connection 1';
// Méthode (dhcp ou static)
$method = trim(shell_exec("nmcli -g ipv4.method con show " . escapeshellarg($conName) . " 2>/dev/null") ?? 'auto');
// IP actuelle
$ip4 = trim(shell_exec("nmcli -g IP4.ADDRESS dev show eth0 2>/dev/null | head -1") ?? '');
// Gateway actuelle
$gw = trim(shell_exec("nmcli -g IP4.GATEWAY dev show eth0 2>/dev/null | head -1") ?? '');
// DNS
$dns = trim(shell_exec("nmcli -g IP4.DNS dev show eth0 2>/dev/null | head -3 | tr '\n' ','") ?? '');
$dns = rtrim($dns, ',');
// Config statique configurée (pas forcément active)
$staticIp = trim(shell_exec("nmcli -g ipv4.addresses con show " . escapeshellarg($conName) . " 2>/dev/null") ?? '');
$staticGw = trim(shell_exec("nmcli -g ipv4.gateway con show " . escapeshellarg($conName) . " 2>/dev/null") ?? '');
$staticDns = trim(shell_exec("nmcli -g ipv4.dns con show " . escapeshellarg($conName) . " 2>/dev/null") ?? '');
return [
'connection' => $conName,
'method' => ($method === 'auto' || $method === 'dhcp') ? 'dhcp' : 'static',
'current_ip' => $ip4,
'current_gw' => $gw,
'current_dns'=> $dns,
'static_ip' => $staticIp,
'static_gw' => $staticGw,
'static_dns' => $staticDns,
'hostname' => gethostname(),
];
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
echo json_encode(get_network_info());
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true) ?? [];
$mode = $data['mode'] ?? 'dhcp';
if ($mode === 'dhcp') {
$out = shell_exec('sudo /opt/copymail/scripts/apply_network.sh dhcp 2>&1');
echo json_encode(['success' => true, 'msg' => trim($out)]);
exit;
}
if ($mode === 'static') {
$ip = trim($data['ip'] ?? '');
$prefix = trim($data['prefix'] ?? '24');
$gateway = trim($data['gateway'] ?? '');
$dns = trim($data['dns'] ?? '8.8.8.8,1.1.1.1');
// Validation basique
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
http_response_code(400);
echo json_encode(['error' => 'Adresse IP invalide.']);
exit;
}
if (!filter_var($gateway, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
http_response_code(400);
echo json_encode(['error' => 'Passerelle invalide.']);
exit;
}
$prefix = max(1, min(30, (int)$prefix));
$out = shell_exec(sprintf(
'sudo /opt/copymail/scripts/apply_network.sh static %s %s %s 2>&1',
escapeshellarg("{$ip}/{$prefix}"),
escapeshellarg($gateway),
escapeshellarg($dns)
));
echo json_encode(['success' => true, 'msg' => trim($out), 'new_ip' => $ip]);
exit;
}
http_response_code(400);
echo json_encode(['error' => 'Mode invalide.']);
}