DHCP: Auto-Write+Neustart nach Änderungen, Lease löschen, Dedup, Autostart-Slider

- Automatisches Schreiben+Neustart: jede relevante Änderung (Reservierung,
  Option, Subnetz, globale Einstellung) ruft jetzt _dhcp_write_and_restart()
  direkt nach dem Speichern auf -- schreibt die Kea-Config neu und startet
  den Dienst neu, FALLS er gerade aktiv ist (kein ungefragtes Hochfahren
  eines bewusst gestoppten Dienstes). Ein manuelles "Datei schreiben" +
  "Neu starten" kann dadurch nicht mehr vergessen werden. Der bisherige
  "Speichern"-Button (write_dhcp_file) restartet jetzt ebenfalls
  automatisch statt nur zu schreiben.
- Leases-Übersicht dedupliziert: Keas memfile-Backend hängt bei jeder
  Verlängerung eine neue Zeile an, statt die alte zu ersetzen -- dieselbe
  Adresse stand oft dutzendfach in der CSV. _dhcp_read_leases() zeigt jetzt
  nur noch die jeweils neueste (per "expire") Zeile pro IP.
- Einzelne Leases löschen (neuer Button + Route): entfernt alle Zeilen
  einer Adresse aus der Lease-CSV und startet den Dienst neu, damit Kea sie
  beim Neuladen nicht aus dem Speicher zurückschreibt.
- Eigenständiger Autostart-Slider (wie beim Aktivieren/Deaktivieren von
  Clients) -- steuert nur systemctl enable/disable, entkoppelt vom
  aktuellen Start/Stopp-Zustand.
- KRITISCH, live gefunden und behoben: die erste Version von
  _dhcp_delete_lease() hat Kea beim Neustart zum Absturz gebracht --
  Pythons csv-Modul schreibt standardmäßig "\r\n"-Zeilenenden, Kea erwartet
  aber reines "\n" und lehnte die Datei mit "invalid column: 'pool_id^M'"
  komplett ab. Zusätzlich hätte das Neuanlegen der Datei als root (der
  App-Prozess) sie root:root statt dem Kea-Dienstnutzer gehören lassen,
  wodurch Kea keine neuen Leases mehr hätte schreiben können. Beides
  gefixt (lineterminator="\n", Eigentümer/Rechte der Originaldatei nach dem
  Schreiben wiederhergestellt) und auf POETEST bestätigt: Dienst übersteht
  den Neustart jetzt, Datei bleibt _kea:_kea, keine ^M mehr.
This commit is contained in:
2026-08-13 19:11:34 +02:00
parent 4c13127679
commit 93bcf4f6b6
2 changed files with 220 additions and 10 deletions
+42 -4
View File
@@ -24,10 +24,17 @@
{% else %}
<span class="pill unknown">Dienst-Status unbekannt</span>
{% endif %}
{% if status.enabled is sameas true %}<span class="pill user">Autostart an</span>{% endif %}
{% if not can_edit and status.enabled is sameas true %}<span class="pill user">Autostart an</span>{% endif %}
</div>
{% if status.version %}<div class="text-faint mono" style="font-size:12px; margin-bottom:12px;">{{ status.version }}</div>{% endif %}
{% if can_edit %}
<div class="flex gap-2" style="align-items:center; margin-bottom:14px;">
<label class="switch-check">
<input type="checkbox" id="dhcpAutostartToggle" {% if status.enabled %}checked{% endif %} onchange="toggleDhcpAutostart(this)">
<span class="track"></span>
</label>
<span style="font-size:13px;">Autostart beim Hochfahren des Hosts</span>
</div>
<div class="flex gap-2" style="flex-wrap:wrap;">
<form method="post" data-confirm="{{ dhcp_service }} aktivieren und (neu) starten? Übernimmt die zuletzt geschriebene Konfiguration.">
<input type="hidden" name="dhcp_service_action" value="enable_restart">
@@ -76,7 +83,7 @@
</div>
<div class="field"><label>Ausgabepfad (Kea-Konfigurationsdatei)</label>
<input type="text" name="dhcp_output_path" value="{{ cfg.dhcp_output_path }}" class="mono">
<div class="field-hint">Standardmäßig die aktive Kea-Konfiguration. Nach dem Schreiben den Dienst per Button neu starten, damit die Änderung wirksam wird.</div>
<div class="field-hint">Standardmäßig die aktive Kea-Konfiguration. Läuft der Dienst gerade, schreibt „Speichern“ die Datei neu und startet ihn automatisch neu.</div>
</div>
<button type="submit" class="btn btn-primary btn-block">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
@@ -281,7 +288,7 @@
<div class="table-wrap">
<div style="overflow-x:auto;">
<table class="data-table">
<thead><tr><th>IP-Adresse</th><th>MAC</th><th>Hostname</th><th>Läuft ab</th><th>Status</th></tr></thead>
<thead><tr><th>IP-Adresse</th><th>MAC</th><th>Hostname</th><th>Läuft ab</th><th>Status</th>{% if can_edit %}<th style="width:1%;">Aktionen</th>{% endif %}</tr></thead>
<tbody>
{% for l in leases %}
<tr>
@@ -305,6 +312,16 @@
</span>
{% endif %}
</td>
{% if can_edit %}
<td>
<form method="post" action="{{ url_for('delete_dhcp_lease') }}" data-confirm="Lease {{ l.ip }} ({{ l.mac }}) löschen? Der DHCP-Dienst wird dabei kurz neu gestartet, falls er gerade läuft.">
<input type="hidden" name="ip" value="{{ l.ip }}">
<button type="submit" class="icon-btn" style="color:var(--danger);" title="Lease löschen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6h16z"/><path d="M10 11v6M14 11v6"/></svg>
</button>
</form>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
@@ -332,7 +349,7 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
Reservierung hinzufügen
</button>
<form method="post" data-confirm="Konfiguration nach „{{ cfg.dhcp_output_path }}“ schreiben? Der Dienst übernimmt die Änderung erst nach einem Neustart.">
<form method="post" data-confirm="Konfiguration nach „{{ cfg.dhcp_output_path }}“ schreiben? Läuft der Dienst gerade, wird er dabei automatisch neu gestartet, damit die Änderung sofort wirksam wird.">
<input type="hidden" name="write_dhcp_file" value="1">
<button type="submit" class="btn btn-primary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/></svg>
@@ -633,3 +650,24 @@
{% endif %}
{% endblock %}
{% block scripts %}
<script>
function toggleDhcpAutostart(checkbox) {
checkbox.disabled = true;
fetch("{{ url_for('toggle_dhcp_autostart') }}", { method: "POST" })
.then(r => r.json())
.then(data => {
checkbox.disabled = false;
if (data.success) {
checkbox.checked = data.new_status === 1;
showToast(data.msg, "success");
} else {
checkbox.checked = !checkbox.checked;
showToast(data.msg, "danger");
}
})
.catch(() => { checkbox.disabled = false; checkbox.checked = !checkbox.checked; showToast("Fehler beim Umschalten.", "danger"); });
}
</script>
{% endblock %}