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:
+178
-6
@@ -18,7 +18,7 @@ from cryptography.fernet import Fernet
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from datetime import datetime, timedelta
|
||||
import base64, csv, ipaddress, logging, shlex, shutil, socket, sqlite3, glob, json, os, re, secrets, subprocess, threading, time, traceback
|
||||
import base64, csv, ipaddress, logging, shlex, shutil, socket, sqlite3, glob, json, os, re, secrets, stat, subprocess, threading, time, traceback
|
||||
import paramiko
|
||||
import yaml
|
||||
import ssl
|
||||
@@ -3285,7 +3285,14 @@ def _dhcp_read_leases():
|
||||
except OSError:
|
||||
return None
|
||||
now = int(time.time())
|
||||
leases = []
|
||||
# Kea's memfile-Backend HÄNGT bei jeder Verlängerung eine neue Zeile an,
|
||||
# statt die alte zu ersetzen -- dieselbe Adresse steht oft dutzendfach
|
||||
# in der Datei, mit jeweils weiter in die Zukunft geschobenem "expire".
|
||||
# Da Kea eine Lease schon bei ~50% der Laufzeit erneuert (nicht erst bei
|
||||
# Ablauf), sind kurz nach einer Verlängerung sowohl die alte als auch
|
||||
# die neue Zeile noch "nicht abgelaufen" -- ohne Deduplizierung nach
|
||||
# Adresse würde dieselbe Lease doppelt in der Übersicht auftauchen.
|
||||
by_ip = {}
|
||||
for row in rows:
|
||||
# state: 0 = aktiv/default, 1 = "declined" (Konflikt erkannt),
|
||||
# 2 = "expired-reclaimed" (von Kea bereits aufgeräumt) — nur 0 ist
|
||||
@@ -3300,8 +3307,16 @@ def _dhcp_read_leases():
|
||||
# noch nicht aus der CSV entfernt — nicht als aktiv anzeigen.
|
||||
if expire and expire < now:
|
||||
continue
|
||||
ip = row.get("address") or ""
|
||||
existing = by_ip.get(ip)
|
||||
if existing is not None and (existing.get("expire") or 0) >= expire:
|
||||
continue # bereits eine neuere (oder gleich neue) Zeile für diese IP gesehen
|
||||
by_ip[ip] = {"expire": expire, "row": row}
|
||||
leases = []
|
||||
for ip, entry in by_ip.items():
|
||||
row, expire = entry["row"], entry["expire"]
|
||||
leases.append({
|
||||
"ip": row.get("address") or "",
|
||||
"ip": ip,
|
||||
"mac": (row.get("hwaddr") or "").upper(),
|
||||
"hostname": row.get("hostname") or "",
|
||||
# Absolut UND als Epoch-ms für einen live tickenden Countdown im
|
||||
@@ -3320,6 +3335,58 @@ def _dhcp_read_leases():
|
||||
return leases
|
||||
|
||||
|
||||
def _dhcp_delete_lease(ip):
|
||||
"""Entfernt ALLE Zeilen einer Adresse direkt aus der Kea-Lease-CSV
|
||||
(memfile-Backend, siehe _dhcp_read_leases für dieselbe Datei) und
|
||||
startet den laufenden Dienst neu, damit Kea die Datei beim Hochfahren
|
||||
frisch neu einliest -- ein bloßes Editieren bei laufendem Dienst würde
|
||||
sonst von Keas eigenem, periodischem Lease-File-Cleanup irgendwann
|
||||
wieder mit dem alten Stand aus dem Speicher überschrieben. Startet den
|
||||
Dienst nur neu, wenn er gerade aktiv ist (kein ungefragtes Hochfahren
|
||||
eines bewusst gestoppten Dienstes). Gibt (ok, message) zurück."""
|
||||
try:
|
||||
original_stat = os.stat(KEA_LEASE_FILE)
|
||||
with open(KEA_LEASE_FILE, newline="", encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f)
|
||||
fieldnames = reader.fieldnames
|
||||
rows = list(reader)
|
||||
except OSError:
|
||||
return False, "Keine Lease-Datei gefunden."
|
||||
remaining = [r for r in rows if (r.get("address") or "") != ip]
|
||||
if len(remaining) == len(rows):
|
||||
return False, f"Keine Lease für {ip} in der Datei gefunden."
|
||||
try:
|
||||
# lineterminator="\n" ist entscheidend: der csv-Modul-Default ist
|
||||
# "\r\n" (RFC 4180), Keas eigener Writer schreibt aber reines "\n" --
|
||||
# ohne diese explizite Angabe interpretiert Kea beim nächsten Start
|
||||
# das "\r" als Teil des letzten Spaltennamens ("pool_id\r") und
|
||||
# lehnt die ganze Datei mit "invalid column" ab (live reproduziert).
|
||||
with open(KEA_LEASE_FILE, "w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames, lineterminator="\n")
|
||||
writer.writeheader()
|
||||
writer.writerows(remaining)
|
||||
# Diese App läuft als root, öffnet die Datei aber mit "w" -- das
|
||||
# legt sie neu an und würde sie root:root statt dem Kea-Dienstnutzer
|
||||
# gehören lassen. Kea (i.d.R. eigener Systemnutzer, z.B. "_kea")
|
||||
# kann dann zwar noch lesen, aber keine neuen Leases mehr
|
||||
# hineinschreiben. Eigentümer/Rechte der Originaldatei deshalb
|
||||
# explizit wiederherstellen (live reproduziert).
|
||||
os.chown(KEA_LEASE_FILE, original_stat.st_uid, original_stat.st_gid)
|
||||
os.chmod(KEA_LEASE_FILE, stat.S_IMODE(original_stat.st_mode))
|
||||
except OSError as e:
|
||||
return False, f"Lease-Datei konnte nicht geschrieben werden: {e}"
|
||||
try:
|
||||
status = subprocess.run(["systemctl", "is-active", DHCP_SERVICE], capture_output=True, text=True, timeout=5)
|
||||
active = status.stdout.strip() == "active"
|
||||
except Exception:
|
||||
active = False
|
||||
if active:
|
||||
ok_restart, restart_out = _dhcp_run_privileged(["systemctl", "restart", DHCP_SERVICE], timeout=30)
|
||||
if not ok_restart:
|
||||
return False, f"Lease für {ip} aus der Datei entfernt, Neustart des Dienstes fehlgeschlagen: {restart_out}"
|
||||
return True, f"Lease für {ip} gelöscht."
|
||||
|
||||
|
||||
def _render_kea_config(cfg, valid_subnets, reservations_by_subnet, option_defs, option_values):
|
||||
"""Baut die komplette Kea-DHCPv4-Konfiguration (als dict, JSON-
|
||||
serialisierbar) — ein subnet4-Block je konfiguriertem Subnetz, damit ein
|
||||
@@ -3452,6 +3519,49 @@ def _current_timezone():
|
||||
return out.strip() if ok else None
|
||||
|
||||
|
||||
def _dhcp_write_and_restart(conn):
|
||||
"""Schreibt die Kea-Konfiguration aus dem aktuellen DB-Stand neu und
|
||||
startet den Dienst neu, FALLS er gerade aktiv ist -- aufgerufen direkt
|
||||
nach jeder Änderung, die sich auf die geschriebene Konfiguration
|
||||
auswirkt (Reservierung, Option, Subnetz, globale Einstellung), damit
|
||||
eine Änderung sofort wirksam wird und ein manuelles "Datei schreiben" +
|
||||
"Neu starten" nicht mehr vergessen werden kann. Schreibt nur, wenn
|
||||
mindestens ein gültiges Subnetz konfiguriert ist (sonst gäbe es nichts
|
||||
Sinnvolles zu schreiben); startet nur neu, wenn der Dienst bereits
|
||||
läuft -- ein bewusst gestoppter Dienst wird dadurch nicht ungefragt
|
||||
wieder hochgefahren. Flasht nur bei einem tatsächlichen Fehler etwas
|
||||
Zusätzliches, ein stiller Erfolg reiht sich in die ohnehin schon
|
||||
gezeigte Erfolgsmeldung der jeweiligen Aktion ein."""
|
||||
cfg = _load_dhcp_settings()
|
||||
valid_subnets, _ = _dhcp_valid_subnets(_dhcp_subnets(conn))
|
||||
if not valid_subnets:
|
||||
return
|
||||
reservations_by_subnet, _ = _dhcp_reservation_candidates(conn, valid_subnets)
|
||||
option_defs = _dhcp_option_defs(conn)
|
||||
option_values = _dhcp_option_values(conn)
|
||||
content = json.dumps(
|
||||
_render_kea_config(cfg, valid_subnets, reservations_by_subnet, option_defs, option_values),
|
||||
indent=2, ensure_ascii=False,
|
||||
)
|
||||
output_path = cfg["dhcp_output_path"]
|
||||
try:
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(content + "\n")
|
||||
except OSError as e:
|
||||
flash(f"Konfiguration konnte nicht automatisch geschrieben werden: {e}", "danger")
|
||||
return
|
||||
try:
|
||||
status = subprocess.run(["systemctl", "is-active", DHCP_SERVICE], capture_output=True, text=True, timeout=5)
|
||||
active = status.stdout.strip() == "active"
|
||||
except Exception:
|
||||
active = False
|
||||
if active:
|
||||
ok_restart, restart_out = _dhcp_run_privileged(["systemctl", "restart", DHCP_SERVICE], timeout=30)
|
||||
if not ok_restart:
|
||||
flash(f"Konfiguration geschrieben, Neustart des DHCP-Dienstes fehlgeschlagen: {restart_out}", "danger")
|
||||
|
||||
|
||||
@app.route("/settings/dhcp", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def settings_dhcp():
|
||||
@@ -3477,6 +3587,7 @@ def settings_dhcp():
|
||||
set_setting(key, value)
|
||||
log_action("settings.update", "DHCP-Konfiguration")
|
||||
flash("DHCP-Konfiguration gespeichert.", "success")
|
||||
_dhcp_write_and_restart(conn)
|
||||
|
||||
elif "add_dhcp_subnet" in request.form:
|
||||
interface = request.form.get("interface", "").strip()
|
||||
@@ -3500,6 +3611,7 @@ def settings_dhcp():
|
||||
conn.commit()
|
||||
log_action("settings.dhcp_subnet_add", interface, f"{range_start}–{range_end}")
|
||||
flash(f"Subnetz {range_start}–{range_end} auf „{interface}“ hinzugefügt.", "success")
|
||||
_dhcp_write_and_restart(conn)
|
||||
|
||||
elif "edit_dhcp_subnet" in request.form:
|
||||
subnet_id = request.form.get("edit_dhcp_subnet")
|
||||
@@ -3522,6 +3634,7 @@ def settings_dhcp():
|
||||
conn.commit()
|
||||
log_action("settings.dhcp_subnet_edit", interface, f"{range_start}–{range_end}")
|
||||
flash("Subnetz aktualisiert.", "success")
|
||||
_dhcp_write_and_restart(conn)
|
||||
|
||||
elif "delete_dhcp_subnet" in request.form:
|
||||
subnet_id = request.form.get("delete_dhcp_subnet")
|
||||
@@ -3529,6 +3642,7 @@ def settings_dhcp():
|
||||
conn.commit()
|
||||
log_action("settings.dhcp_subnet_delete", subnet_id)
|
||||
flash("Subnetz gelöscht.", "success")
|
||||
_dhcp_write_and_restart(conn)
|
||||
|
||||
elif "toggle_dhcp_subnet" in request.form:
|
||||
subnet_id = request.form.get("toggle_dhcp_subnet")
|
||||
@@ -3543,6 +3657,7 @@ def settings_dhcp():
|
||||
"Subnetz deaktiviert — wird beim nächsten Schreiben/Starten nicht mehr berücksichtigt.",
|
||||
"success",
|
||||
)
|
||||
_dhcp_write_and_restart(conn)
|
||||
|
||||
elif "add_dhcp_option" in request.form:
|
||||
code_raw = request.form.get("code", "").strip()
|
||||
@@ -3570,6 +3685,7 @@ def settings_dhcp():
|
||||
conn.commit()
|
||||
log_action("settings.dhcp_option_add", name, f"Code {code_raw}, Typ {opt_type}")
|
||||
flash(f"Option „{name}“ (Code {code_raw}) angelegt.", "success")
|
||||
_dhcp_write_and_restart(conn)
|
||||
except sqlite3.IntegrityError:
|
||||
flash("Code oder Name wird bereits verwendet.", "danger")
|
||||
|
||||
@@ -3584,6 +3700,7 @@ def settings_dhcp():
|
||||
conn.commit()
|
||||
log_action("settings.dhcp_option_delete", option_def["name"] if option_def else option_def_id)
|
||||
flash("Option gelöscht.", "success")
|
||||
_dhcp_write_and_restart(conn)
|
||||
|
||||
elif "save_dhcp_option_global" in request.form:
|
||||
option_def_id = request.form.get("option_def_id")
|
||||
@@ -3599,6 +3716,7 @@ def settings_dhcp():
|
||||
conn.commit()
|
||||
log_action("settings.dhcp_option_value", option_def_id, "global")
|
||||
flash("Globaler Wert gespeichert.", "success")
|
||||
_dhcp_write_and_restart(conn)
|
||||
|
||||
elif "save_dhcp_device_options" in request.form:
|
||||
mac = request.form.get("mac", "").strip()
|
||||
@@ -3615,6 +3733,7 @@ def settings_dhcp():
|
||||
conn.commit()
|
||||
log_action("settings.dhcp_option_value", mac, "Client-Override")
|
||||
flash(f"DHCP-Optionen für {mac} gespeichert.", "success")
|
||||
_dhcp_write_and_restart(conn)
|
||||
|
||||
elif "add_dhcp_reservation" in request.form:
|
||||
mac = request.form.get("mac", "").strip().upper()
|
||||
@@ -3643,6 +3762,7 @@ def settings_dhcp():
|
||||
conn.commit()
|
||||
log_action("settings.dhcp_reservation_add", mac, f"{ip} ({name})")
|
||||
flash(f"Manuelle Reservierung für {mac} hinzugefügt.", "success")
|
||||
_dhcp_write_and_restart(conn)
|
||||
except sqlite3.IntegrityError:
|
||||
flash(f"Für MAC {mac} existiert bereits eine manuelle Reservierung.", "danger")
|
||||
|
||||
@@ -3653,6 +3773,7 @@ def settings_dhcp():
|
||||
conn.commit()
|
||||
log_action("settings.dhcp_reservation_delete", row["mac"] if row else reservation_id)
|
||||
flash("Manuelle Reservierung gelöscht.", "success")
|
||||
_dhcp_write_and_restart(conn)
|
||||
|
||||
elif "write_dhcp_file" in request.form:
|
||||
cfg = _load_dhcp_settings()
|
||||
@@ -3682,15 +3803,25 @@ def settings_dhcp():
|
||||
"settings.dhcp_write", output_path,
|
||||
f"{len(valid_subnets)} Subnetz(e), {total_reservations} Reservierungen",
|
||||
)
|
||||
try:
|
||||
status = subprocess.run(["systemctl", "is-active", DHCP_SERVICE], capture_output=True, text=True, timeout=5)
|
||||
active = status.stdout.strip() == "active"
|
||||
except Exception:
|
||||
active = False
|
||||
restart_ok, restart_out = (True, "")
|
||||
if active:
|
||||
restart_ok, restart_out = _dhcp_run_privileged(["systemctl", "restart", DHCP_SERVICE], timeout=30)
|
||||
msg = (
|
||||
f"{len(valid_subnets)} Subnetz(e) mit insgesamt {total_reservations} Reservierungen nach "
|
||||
f"{output_path} geschrieben. Der Dienst übernimmt die Änderung erst nach einem Neustart "
|
||||
f"(Button unten) — das führt die App bewusst nicht automatisch mit aus."
|
||||
f"{output_path} geschrieben"
|
||||
+ (" und Dienst neu gestartet." if active and restart_ok else "." if not active else "")
|
||||
)
|
||||
if active and not restart_ok:
|
||||
msg += f" Neustart des Dienstes fehlgeschlagen: {restart_out}"
|
||||
if invalid_subnets:
|
||||
skipped = ", ".join(f"{s['interface']} ({s['range_start']}–{s['range_end']})" for s, _ in invalid_subnets)
|
||||
msg += f" Übersprungen, da zu keinem echten Netz passend: {skipped}."
|
||||
flash(msg, "success")
|
||||
flash(msg, "success" if (not active or restart_ok) else "danger")
|
||||
except OSError as e:
|
||||
flash(f"Konnte Datei nicht schreiben: {e}", "danger")
|
||||
|
||||
@@ -3802,6 +3933,47 @@ def settings_dhcp():
|
||||
)
|
||||
|
||||
|
||||
@app.route("/settings/dhcp/lease/delete", methods=["POST"])
|
||||
@login_required
|
||||
def delete_dhcp_lease():
|
||||
if not current_user.has_permission("settings_dhcp.edit"):
|
||||
flash("Keine Berechtigung, Leases zu löschen.", "danger")
|
||||
return redirect(url_for("settings_dhcp"))
|
||||
ip = request.form.get("ip", "").strip()
|
||||
ok, message = _dhcp_delete_lease(ip)
|
||||
log_action("settings.dhcp_lease_delete", ip, message)
|
||||
flash(message, "success" if ok else "danger")
|
||||
return redirect(url_for("settings_dhcp"))
|
||||
|
||||
|
||||
@app.route("/settings/dhcp/autostart/toggle", methods=["POST"])
|
||||
@login_required
|
||||
def toggle_dhcp_autostart():
|
||||
"""Eigenständiger Autostart-Schalter, entkoppelt vom aktuellen
|
||||
Start/Stopp-Zustand -- steuert ausschließlich, ob systemd den Dienst
|
||||
bei einem Host-Neustart automatisch mitstartet (systemctl enable/
|
||||
disable), OHNE ihn hier auch tatsächlich zu (de)starten."""
|
||||
if not current_user.has_permission("settings_dhcp.edit"):
|
||||
return {"success": False, "msg": "Keine Berechtigung."}, 403
|
||||
try:
|
||||
result = subprocess.run(["systemctl", "is-enabled", DHCP_SERVICE], capture_output=True, text=True, timeout=5)
|
||||
currently_enabled = result.stdout.strip() == "enabled"
|
||||
except Exception:
|
||||
currently_enabled = False
|
||||
action = "disable" if currently_enabled else "enable"
|
||||
ok, out = _dhcp_run_privileged(["systemctl", action, DHCP_SERVICE], timeout=15)
|
||||
if not ok:
|
||||
verb = "deaktivieren" if currently_enabled else "aktivieren"
|
||||
return {"success": False, "msg": f"Autostart konnte nicht {verb} werden: {out}"}
|
||||
new_enabled = not currently_enabled
|
||||
log_action("settings.dhcp_autostart", DHCP_SERVICE, "aktiviert" if new_enabled else "deaktiviert")
|
||||
return {
|
||||
"success": True,
|
||||
"msg": f"Autostart {'aktiviert' if new_enabled else 'deaktiviert'}.",
|
||||
"new_status": 1 if new_enabled else 0,
|
||||
}
|
||||
|
||||
|
||||
@app.route("/settings/ldap", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def settings_ldap():
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user