DHCP: Mehrfach-Subnetze, aktive Leases-Anzeige, Reboot-Persistenz
1. Mehrere gleichzeitige DHCP-Subnetze statt einer einzelnen globalen Range
-- ein Host kann mehrere IPs/Interfaces mit jeweils eigenem Netz haben,
fuer die alle DHCP angeboten werden soll:
- Neue Tabelle dhcp_subnets (interface, range_start/end, optionales
gateway/dns je Zeile) statt der bisherigen dhcp_interface/_range_*/
_gateway/_dns-Settings-Keys. Domain, Lease-Zeiten und Ausgabepfad
bleiben global (Kea-weit gueltig).
- Einmalige Migration: eine bestehende globale Einzel-Range wird beim
ersten Start automatisch in eine erste Subnetz-Zeile ueberfuehrt,
statt eine funktionierende Konfiguration beim Upgrade zu verlieren
(live verifiziert: bestehende 192.168.80.220-230-Range korrekt
uebernommen).
- Neue Routen add/edit/delete_dhcp_subnet, jede mit derselben
Sicherheitsregel wie zuvor: es muss eine physische IP im gewuenschten
Bereich vorhanden sein (_dhcp_matching_network je Subnetz-Zeile),
sonst wird die Aktion abgelehnt. _detect_interface_network wurde dafuer
in eine neue _detect_interface_networks (alle IPv4-Adressen eines
Interfaces, nicht nur die erste -- ein Interface kann mehrere IPs
tragen) plus einen duennen Wrapper fuer die primaere Adresse aufgeteilt.
- _render_kea_config generiert jetzt einen eigenen subnet4-Block je
Subnetz (eigenes Gateway/DNS-option-data), _dhcp_reservation_candidates
ordnet jedes Geraet anhand seiner IP dem richtigen Subnetz zu.
- "In Datei schreiben" ueberspringt inzwischen ungueltig gewordene
Subnetze einzeln (mit Warnung, welche) statt die ganze Aktion
abzubrechen; "Aktivieren & (neu) starten" verweigert den Start, falls
kein einziges gueltiges Subnetz mehr existiert.
- Live auf der Test-VM verifiziert: Anlegen/Bearbeiten/Loeschen inkl.
Ablehnung ungueltiger Ranges, generierte Kea-Config mit zwei
subnet4-Bloecken korrekt (Interface, Subnet-CIDR, Pools je Zeile).
2. Aktive Leases direkt aus Kea sichtbar (settings_dhcp.html, neue Karte)
-- macht Clients OHNE eigene Reservierung sichtbar, die sich einfach
eine freie IP aus dem Pool genommen haben, statt nur die (unvollstaendige)
Reservierungsliste zu zeigen. Liest die memfile-Lease-CSV direkt
(kein Kea-Control-Agent noetig), reserved-Flag durch Abgleich mit den
bekannten Reservierungs-MACs. Live gegen eine echte (leere)
kea-leases4.csv verifiziert.
3. DHCP-Dienst uebersteht einen Host-Neustart jetzt korrekt im zuletzt
bewusst gewaehlten Zustand: "Stoppen" deaktiviert den Dienst zusaetzlich
(nicht nur systemctl stop), sonst wuerde systemd ihn nach einem Neustart
automatisch wieder hochfahren, obwohl der Admin ihn bewusst abgeschaltet
hat. Dieselbe Ergaenzung im automatischen Netzwerkaenderungs-Stop (siehe
vorheriger Commit) -- sonst koennte ein Host-Neustart vor der
Range-Pruefung den Dienst trotzdem mit einer ggf. falschen Konfiguration
wieder starten.
This commit is contained in:
@@ -229,20 +229,42 @@ der aktiv weiterentwickelte Nachfolger und bildet „globaler Wert, pro Client
|
||||
- **Installation & Dienststeuerung**: eigene, jeweils einzeln bestätigte
|
||||
Buttons — „kea-dhcp4-server installieren“ (`apt-get install -y`),
|
||||
„Aktivieren & (neu) starten“ (`systemctl enable` + `restart`) und
|
||||
„Stoppen“. Bewusst **nicht** automatisch an „Konfiguration speichern“
|
||||
oder „In Datei schreiben“ gekoppelt — ein unerwartet aktiver oder
|
||||
fehlkonfigurierter DHCP-Server kann ein ganzes Netzsegment stören, jede
|
||||
dieser Aktionen soll ein bewusster, separater Klick bleiben.
|
||||
- **Netzwerk-Kopplung**: Subnet, Netzmaske und Gateway werden **nie manuell
|
||||
„Stoppen“ (`systemctl stop` **+ `disable`**, damit ein bewusst gestoppter
|
||||
Dienst nach einem Host-Neustart nicht trotzdem automatisch wieder
|
||||
hochfährt — ein Host-Neustart übernimmt so genau den zuletzt bewusst
|
||||
gewählten Zustand, an oder aus). Bewusst **nicht** automatisch an
|
||||
„Konfiguration speichern“ oder „In Datei schreiben“ gekoppelt — ein
|
||||
unerwartet aktiver oder fehlkonfigurierter DHCP-Server kann ein ganzes
|
||||
Netzsegment stören, jede dieser Aktionen soll ein bewusster, separater
|
||||
Klick bleiben.
|
||||
- **Mehrere Subnetze gleichzeitig**: ein Host kann mehrere IPs/Interfaces
|
||||
mit jeweils eigenem Netz haben, für die alle DHCP angeboten werden soll
|
||||
(`dhcp_subnets`-Tabelle statt einer einzelnen globalen Range) — jedes
|
||||
Subnetz hat eigene Interface/Range sowie optional eigenes Gateway/DNS,
|
||||
generiert wird ein eigener `subnet4`-Block je Subnetz in der Kea-Config.
|
||||
Domain, Lease-Zeiten und Ausgabepfad bleiben global (Kea-weit gültig).
|
||||
- **Netzwerk-Kopplung**: Subnet und Netzmaske werden **nie manuell
|
||||
eingegeben**, sondern bei jeder Anzeige/Generierung live aus der
|
||||
System-Netzwerkkonfiguration des gewählten Interfaces gelesen
|
||||
(`_detect_interface_network()`: `ip -4 addr show dev <iface>` +
|
||||
`ip route show default`). Die generierte Kea-Konfiguration ist dadurch
|
||||
immer an das Netz gekoppelt, in dem der Host tatsächlich hängt, auch wenn
|
||||
sich dessen IP/Subnet mal ändert — nur das Interface selbst (Dropdown,
|
||||
echte vorhandene Interfaces aus `/sys/class/net`), die Lease-Range,
|
||||
DNS-Server, Domain, Lease-Zeiten und der Ausgabepfad sind konfigurierbar
|
||||
(`dhcp_*`-Schlüssel in der `settings`-Tabelle).
|
||||
System-Netzwerkkonfiguration des jeweiligen Interfaces gelesen
|
||||
(`_detect_interface_network()`/`_detect_interface_networks()`:
|
||||
`ip -4 addr show dev <iface>` + `ip route show default`) — berücksichtigt
|
||||
dabei ALLE IPv4-Adressen eines Interfaces, nicht nur die erste (ein
|
||||
Interface kann mehrere IPs/Subnetze gleichzeitig tragen, z.B. Alias-IPs).
|
||||
Die generierte Kea-Konfiguration ist dadurch immer an das Netz gekoppelt,
|
||||
in dem der Host tatsächlich hängt, auch wenn sich dessen IP/Subnet mal
|
||||
ändert.
|
||||
- **Range ist Pflicht und muss zu einem echten Netz passen**: kein
|
||||
vorausgefüllter Default für Range/DNS (nur Platzhaltertext) — ein
|
||||
Subnetz ohne zu einem tatsächlich vorhandenen Netz passende Range wird
|
||||
beim Anlegen/Bearbeiten abgelehnt (`_dhcp_matching_network()`), da Kea
|
||||
ohne eine echte, physisch vorhandene IP im gewünschten Bereich keine
|
||||
gültige Subnet-CIDR zugeordnet werden kann. Dieselbe Prüfung läuft
|
||||
zusätzlich bei „In Datei schreiben“ (Subnetze, die inzwischen zu keinem
|
||||
echten Netz mehr passen — z.B. nach einem Netzwechsel des Hosts — werden
|
||||
übersprungen und in der Erfolgsmeldung benannt, statt die ganze Aktion
|
||||
abzubrechen) und bei „Aktivieren & (neu) starten“ (verweigert den Start,
|
||||
falls kein einziges gültiges Subnetz mehr existiert). Live auf einer
|
||||
echten Fehlkonfiguration reproduziert und verifiziert.
|
||||
- **Eigene Options** (`dhcp_option_defs`/`dhcp_option_values`, analog den
|
||||
„Vordefinierten Optionen und Werten“ eines Windows-DHCP-Servers): Code,
|
||||
Name, Kea-Datentyp und Beschreibung frei definierbar (z.B. herstellerspezifische
|
||||
@@ -252,33 +274,25 @@ der aktiv weiterentwickelte Nachfolger und bildet „globaler Wert, pro Client
|
||||
(technisch: globale Werte landen im Top-Level `option-data`, Overrides im
|
||||
`option-data` der jeweiligen Kea-Reservierung, was Kea automatisch pro
|
||||
Client Vorrang geben lässt).
|
||||
- **Reservierungen**: für jedes aktive Gerät mit gültiger MAC + IP **innerhalb
|
||||
des erkannten Subnets** wird eine Kea-Reservierung erzeugt; der
|
||||
- **Reservierungen**: für jedes aktive Gerät mit gültiger MAC + IP wird eine
|
||||
Kea-Reservierung im **passenden Subnetz** erzeugt (die IP entscheidet, zu
|
||||
welchem der ggf. mehreren konfigurierten Subnetze ein Gerät gehört); der
|
||||
Hostname wird aus dem Gerätenamen abgeleitet (nur `a-z0-9-`, Kollisionen
|
||||
bekommen automatisch ein `-2`/`-3`-Suffix). Geräte ohne MAC/IP oder mit
|
||||
einer IP außerhalb des Subnets werden mit Hinweis übersprungen — Kea lehnt
|
||||
eine Reservierung außerhalb ihres Subnets als Konfigurationsfehler ab
|
||||
(mit einer echten Kea-Instanz verifiziert).
|
||||
innerhalb desselben Subnetzes bekommen automatisch ein `-2`/`-3`-Suffix).
|
||||
Geräte ohne MAC/IP oder mit einer IP außerhalb aller konfigurierten
|
||||
Subnetze werden mit Hinweis übersprungen — Kea lehnt eine Reservierung
|
||||
außerhalb ihres Subnets als Konfigurationsfehler ab (mit einer echten
|
||||
Kea-Instanz verifiziert).
|
||||
- **Aktive Leases**: eigene Karte, liest die tatsächlich vergebenen Adressen
|
||||
direkt aus Keas Lease-Datenbank (`kea-leases4.csv`, memfile-Backend —
|
||||
kein Control-Agent nötig, reiner Lesezugriff auf die Datei). Zeigt damit
|
||||
auch Clients **ohne eigene Reservierung**, die sich einfach eine freie IP
|
||||
aus dem Pool genommen haben — deutlich als „Ohne Reservierung“ markiert,
|
||||
statt nur die (unvollständige) Reservierungsliste zu zeigen.
|
||||
- **Schreiben**: „In Datei schreiben“ (nur mit `settings_dhcp.edit`)
|
||||
schreibt die vollständige, generierte Kea-JSON-Konfiguration an den
|
||||
konfigurierten Ausgabepfad. Wirksam wird sie erst nach einem
|
||||
Dienst-Neustart über den separaten Button.
|
||||
- **Range ist Pflicht und muss zu einem echten Netz passen**: kein
|
||||
vorausgefüllter Default für Range/DNS mehr (nur Platzhaltertext) — ohne
|
||||
eingetragene Range schreibt „In Datei schreiben“ nichts und „Aktivieren &
|
||||
(neu) starten“ verweigert den Start. Zusätzlich wird die Range beim
|
||||
Speichern, Schreiben UND Starten gegen die tatsächlich am gewählten
|
||||
Interface vorhandenen IPv4-Netze geprüft (ein Interface kann mehrere IPs/
|
||||
Subnetze gleichzeitig tragen, z.B. Alias-IPs — alle werden berücksichtigt).
|
||||
Passt die Range zu keinem davon (z.B. weil sich das Netz des Hosts seit
|
||||
dem letzten Speichern geändert hat), wird die Aktion mit einer genauen
|
||||
Fehlermeldung abgelehnt, statt den Dienst mit einer zum echten Netz nicht
|
||||
passenden Range laufen zu lassen — live auf einer echten Fehlkonfiguration
|
||||
reproduziert und verifiziert. Gateway ist separat und optional
|
||||
überschreibbar (Default: automatisch erkanntes Gateway des Interfaces).
|
||||
(Mehrere DHCP-Ranges gleichzeitig — z.B. auf unterschiedlichen Interfaces
|
||||
— sind aktuell nicht unterstützt, die Konfiguration ist auf eine globale
|
||||
Range/ein Interface ausgelegt; als nächster Ausbauschritt vorgesehen.)
|
||||
- **Ampel im Topbar**: neben dem Prüfintervall-Timer zeigt ein grüner/roter
|
||||
Punkt, ob der Kea-Dienst läuft — nur sichtbar mit `settings_dhcp.view`.
|
||||
- Reservierungen mit aktiven eigenen Options zeigen in der Tabelle, welche
|
||||
@@ -290,6 +304,23 @@ Installation, generierte Konfiguration besteht `kea-dhcp4 -t`, Dienststart
|
||||
sowie Client-spezifischer Override erscheinen korrekt getrennt in der
|
||||
Konfiguration.
|
||||
|
||||
Die Mehrfach-Subnetz-Unterstützung selbst (Anlegen/Bearbeiten/Löschen samt
|
||||
Validierung, mehrere generierte `subnet4`-Blöcke mit korrekten Pools/
|
||||
Interfaces, Migration einer bestehenden Einzel-Range) ist live auf einem
|
||||
echten Host verifiziert. Zwei tatsächlich **unabhängige** Netze (z.B. über
|
||||
zwei physische NICs) standen beim Test nicht zur Verfügung — verifiziert
|
||||
wurde daher zusätzlich zwei Ranges innerhalb desselben real vorhandenen
|
||||
Netzes (identischer `subnet`-CIDR, unterschiedliche Pools); ob Kea zwei
|
||||
`subnet4`-Blöcke mit identischem `subnet`-Wert akzeptiert oder als Fehler
|
||||
ablehnt, wurde nicht gegen eine echte Kea-Instanz geprüft — bei tatsächlich
|
||||
unterschiedlichen Netzen (der eigentliche Anwendungsfall) besteht dieses
|
||||
Risiko nicht. Die Leases-Anzeige wurde gegen eine echte (leere)
|
||||
`kea-leases4.csv` verifiziert (korrektes Parsen, keine Endlos-/Absturz-
|
||||
Fälle) — eine tatsächlich unreservierte Lease eines fremden Clients live
|
||||
zu erzeugen und die „Ohne Reservierung“-Markierung damit End-to-End zu
|
||||
zeigen, wurde nicht getestet (hätte einen echten, unkontrollierten
|
||||
DHCP-Request auf dem gemeinsam genutzten Testnetz erfordert).
|
||||
|
||||
## Netzwerkeinstellungen des Hosts (Systemeinstellungen)
|
||||
|
||||
Eigene, klar getrennte Funktion von obigem DHCP-*Server*: hier geht es um
|
||||
@@ -326,13 +357,16 @@ da eine Änderung nicht die Erreichbarkeit kappt).
|
||||
erreichbar ist), rollt die App automatisch auf die gesicherte
|
||||
Konfiguration zurück. Verhindert, dass ein Tippfehler bei IP/Gateway den
|
||||
Host von der Web-UI/SSH abschneidet.
|
||||
- **DHCP-Server wird vorsorglich gestoppt**: eine vorher passende Range
|
||||
(siehe DHCP-Konfiguration oben) kann nach einer IP-/Subnetzänderung
|
||||
plötzlich zum falschen Netz gehören. Läuft Kea beim Anwenden oder beim
|
||||
Auto-Rollback, wird der Dienst deshalb gestoppt (nicht automatisch neu
|
||||
gestartet — die Range muss der Admin aktiv auf der DHCP-Seite prüfen und
|
||||
den Dienst dort bewusst wieder starten, statt dass die App mit einer
|
||||
möglicherweise falschen Konfiguration weiterläuft oder blind rät).
|
||||
- **DHCP-Server wird vorsorglich gestoppt UND deaktiviert**: eine vorher
|
||||
passende Range (siehe DHCP-Konfiguration oben) kann nach einer
|
||||
IP-/Subnetzänderung plötzlich zum falschen Netz gehören. Läuft Kea beim
|
||||
Anwenden oder beim Auto-Rollback, wird der Dienst deshalb gestoppt und
|
||||
deaktiviert (nicht automatisch neu gestartet — die Range muss der Admin
|
||||
aktiv auf der DHCP-Seite prüfen und den Dienst dort bewusst wieder
|
||||
starten). Das zusätzliche Deaktivieren verhindert, dass ein Host-Neustart
|
||||
vor dieser Prüfung den Dienst trotzdem automatisch mit der ggf. falschen
|
||||
Range wieder hochfährt, statt dass die App mit einer möglicherweise
|
||||
falschen Konfiguration weiterläuft oder blind rät.
|
||||
- Live gegen eine echte Ubuntu-24.04-VM verifiziert (Standard-Installation,
|
||||
Netz per DHCP über netplan/systemd-networkd bezogen) — sowohl die
|
||||
Erkennung/Statusanzeige als auch der komplette Anwenden-Pfad inkl. der
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<button type="submit" class="btn btn-secondary btn-sm">Aktivieren & (neu) starten</button>
|
||||
</form>
|
||||
{% if status.active is sameas true %}
|
||||
<form method="post" data-confirm="{{ dhcp_service }} stoppen? Der DHCP-Dienst vergibt danach keine Adressen mehr.">
|
||||
<form method="post" data-confirm="{{ dhcp_service }} stoppen und deaktivieren? Startet nach einem Host-Neustart dann nicht automatisch wieder.">
|
||||
<input type="hidden" name="dhcp_service_action" value="stop">
|
||||
<button type="submit" class="btn btn-secondary btn-sm" style="color:var(--danger);">Stoppen</button>
|
||||
</form>
|
||||
@@ -58,50 +58,13 @@
|
||||
<div class="card card-pad">
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Netzwerk-Konfiguration</h2>
|
||||
<div class="hint">Subnet/Netzmaske/Gateway werden live vom gewählten Interface übernommen, nicht manuell gepflegt.</div>
|
||||
<h2 style="font-size:16px;">Globale DHCP-Einstellungen</h2>
|
||||
<div class="hint">Gelten für den gesamten Dienst, unabhängig von den Subnetzen unten.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if net_info.ok %}
|
||||
<div class="flex gap-2" style="align-items:center; margin-bottom:16px; flex-wrap:wrap;">
|
||||
<span class="pill online">Erkannt</span>
|
||||
<span class="mono" style="font-size:12.5px;">{{ net_info.ip }}/{{ net_info.prefix }} (Netz {{ net_info.network }})</span>
|
||||
{% if net_info.gateway %}<span class="text-faint mono" style="font-size:12px;">Gateway {{ net_info.gateway }}</span>{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex gap-2" style="align-items:center; margin-bottom:16px;">
|
||||
<span class="pill offline">Nicht erkannt</span>
|
||||
<span class="text-faint" style="font-size:12px;">{{ net_info.error }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if can_edit %}
|
||||
<form method="post">
|
||||
<input type="hidden" name="save_dhcp_config" value="1">
|
||||
<div class="field"><label>Interface</label>
|
||||
<select name="dhcp_interface">
|
||||
{% for iface in interfaces %}
|
||||
<option value="{{ iface }}" {% if iface == cfg.dhcp_interface %}selected{% endif %}>{{ iface }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="field-hint">Subnet/Netzmaske/Gateway oben werden für das hier ausgewählte Interface erkannt.</div>
|
||||
</div>
|
||||
<div class="field"><label>Range Start</label>
|
||||
<input type="text" name="dhcp_range_start" value="{{ cfg.dhcp_range_start }}" placeholder="z.B. 192.168.1.100" required>
|
||||
</div>
|
||||
<div class="field"><label>Range Ende</label>
|
||||
<input type="text" name="dhcp_range_end" value="{{ cfg.dhcp_range_end }}" placeholder="z.B. 192.168.1.200" required>
|
||||
<div class="field-hint">Pflichtfeld, muss im oben erkannten Netz liegen — ohne Range startet der Dienst nicht.</div>
|
||||
</div>
|
||||
<div class="field"><label>Gateway (optional)</label>
|
||||
<input type="text" name="dhcp_gateway" value="{{ cfg.dhcp_gateway }}" placeholder="Automatisch: {{ net_info.gateway or '—' }}">
|
||||
<div class="field-hint">Nur bei abweichendem Router für die Clients nötig — leer lassen für das oben erkannte Gateway.</div>
|
||||
</div>
|
||||
<div class="field"><label>DNS-Server (optional)</label>
|
||||
<input type="text" name="dhcp_dns" value="{{ cfg.dhcp_dns }}" placeholder="z.B. 1.1.1.1, 8.8.8.8">
|
||||
<div class="field-hint">Kommagetrennt, falls mehrere.</div>
|
||||
</div>
|
||||
<div class="field"><label>Domain</label>
|
||||
<input type="text" name="dhcp_domain" value="{{ cfg.dhcp_domain }}">
|
||||
</div>
|
||||
@@ -125,6 +88,71 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card card-pad" style="grid-column:1 / -1;">
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Subnetze</h2>
|
||||
<div class="hint">
|
||||
Ein Host kann mehrere IPs/Interfaces mit jeweils eigenem Netz bedienen — je Subnetz eigene Range,
|
||||
optional eigenes Gateway/DNS. Subnet/Netzmaske werden live vom Interface übernommen, nicht manuell gepflegt.
|
||||
</div>
|
||||
</div>
|
||||
{% if can_edit %}
|
||||
<button type="button" class="btn btn-primary" data-open-modal="addSubnetModal">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
Subnetz hinzufügen
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if subnets %}
|
||||
<div class="table-wrap">
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>Interface</th><th>Range</th><th>Gateway</th><th>DNS</th><th>Erkanntes Netz</th><th style="width:1%;">Aktionen</th></tr></thead>
|
||||
<tbody>
|
||||
{% for s in subnets %}
|
||||
{% set st = subnet_status.get(s.id) %}
|
||||
<tr>
|
||||
<td class="mono">{{ s.interface }}</td>
|
||||
<td class="mono">{{ s.range_start }} – {{ s.range_end }}</td>
|
||||
<td class="mono text-faint">{{ s.gateway or '(automatisch)' }}</td>
|
||||
<td class="mono text-faint">{{ s.dns or '—' }}</td>
|
||||
<td>
|
||||
{% if st and st.ok %}
|
||||
<span class="pill online">Erkannt</span>
|
||||
<span class="mono text-faint" style="font-size:11.5px;">{{ st.network }}</span>
|
||||
{% else %}
|
||||
<span class="pill offline">Nicht erkannt</span>
|
||||
{% if st %}<span class="text-faint" style="font-size:11px;">{{ st.error }}</span>{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if can_edit %}
|
||||
<div class="flex gap-2">
|
||||
<button type="button" class="icon-btn" title="Bearbeiten" data-open-modal="editSubnetModal{{ loop.index }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4z"/></svg>
|
||||
</button>
|
||||
<form method="post" data-confirm="Subnetz {{ s.range_start }}–{{ s.range_end }} löschen?">
|
||||
<input type="hidden" name="delete_dhcp_subnet" value="{{ s.id }}">
|
||||
<button type="submit" class="icon-btn" style="color:var(--danger);" title="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 6"/></svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-faint" style="font-size:12.5px;">Noch kein Subnetz konfiguriert — der Dienst kann so nicht gestartet werden.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card card-pad" style="grid-column:1 / -1;">
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
@@ -184,14 +212,61 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card card-pad" style="grid-column:1 / -1;">
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Aktive Leases</h2>
|
||||
<div class="hint">
|
||||
Direkt aus Kea gelesen (nicht aus den Reservierungen) — zeigt auch Clients OHNE eigene Reservierung,
|
||||
die sich einfach eine freie IP aus dem Pool genommen haben.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if leases is none %}
|
||||
<p class="text-faint" style="font-size:12.5px;">
|
||||
Keine Lease-Datei gefunden — der Dienst wurde vermutlich noch nie gestartet oder hat noch keine Adresse vergeben.
|
||||
</p>
|
||||
{% elif leases %}
|
||||
<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>
|
||||
<tbody>
|
||||
{% for l in leases %}
|
||||
<tr>
|
||||
<td class="mono">{{ l.ip }}</td>
|
||||
<td class="mono">{{ l.mac }}</td>
|
||||
<td>{{ l.hostname or '—' }}</td>
|
||||
<td class="text-faint mono" style="font-size:12px;">{{ l.expires_at or '—' }}</td>
|
||||
<td>
|
||||
{% if l.reserved %}
|
||||
<span class="pill online">Reserviert</span>
|
||||
{% else %}
|
||||
<span class="pill unknown" title="Client hat sich diese IP selbst aus dem Pool geholt, ohne eigene Reservierung">
|
||||
⚠ Ohne Reservierung
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-faint" style="font-size:12.5px;">Aktuell keine aktiven Leases vergeben.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card card-pad" style="grid-column:1 / -1;">
|
||||
<div class="section-head" style="margin-bottom:16px;">
|
||||
<div>
|
||||
<h2 style="font-size:16px;">Reservierungen aus den Clients</h2>
|
||||
<div class="hint">
|
||||
{{ reservations|length }} aktive{{ ' Clients' if reservations|length != 1 else 'r Client' }} mit gültiger MAC + IP im konfigurierten Subnet.
|
||||
{{ reservations|length }} aktive{{ ' Clients' if reservations|length != 1 else 'r Client' }} mit gültiger MAC + IP in einem der konfigurierten Subnetze.
|
||||
{% if skipped_count %}{{ skipped_count }} aktive{{ ' Clients' if skipped_count != 1 else 'r Client' }} ohne MAC/IP übersprungen.{% endif %}
|
||||
{% if out_of_subnet_count %}{{ out_of_subnet_count }} aktive{{ ' Clients außerhalb' if out_of_subnet_count != 1 else 'r Client außerhalb' }} des erkannten Netzes übersprungen.{% endif %}
|
||||
{% if out_of_subnet_count %}{{ out_of_subnet_count }} aktive{{ ' Clients außerhalb' if out_of_subnet_count != 1 else 'r Client außerhalb' }} aller konfigurierten Subnetze übersprungen.{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if can_edit %}
|
||||
@@ -209,7 +284,7 @@
|
||||
<div class="table-wrap" style="margin-bottom:18px;">
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>Hostname</th><th>MAC</th><th>IP-Adresse</th><th>Client-Name</th><th>Optionen</th><th style="width:1%;">Aktionen</th></tr></thead>
|
||||
<thead><tr><th>Hostname</th><th>MAC</th><th>IP-Adresse</th><th>Client-Name</th><th>Subnetz</th><th>Optionen</th><th style="width:1%;">Aktionen</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in reservations %}
|
||||
<tr>
|
||||
@@ -217,6 +292,7 @@
|
||||
<td class="mono">{{ r.mac }}</td>
|
||||
<td class="mono">{{ r.ip }}</td>
|
||||
<td>{{ r.name }}</td>
|
||||
<td class="mono text-faint" style="font-size:12px;">{{ r.subnet_label }}</td>
|
||||
<td style="font-size:12px;">
|
||||
{% for d in option_defs %}
|
||||
{% set override = option_values.get(d.id, {}).get(r.mac) %}
|
||||
@@ -323,6 +399,90 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="addSubnetModal">
|
||||
<div class="modal">
|
||||
<form method="post">
|
||||
<input type="hidden" name="add_dhcp_subnet" value="1">
|
||||
<div class="modal-header">
|
||||
<h3>Subnetz hinzufügen</h3>
|
||||
<button type="button" class="modal-close" data-close-modal>×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="field"><label>Interface</label>
|
||||
<select name="interface" required>
|
||||
{% for iface in interfaces %}
|
||||
<option value="{{ iface }}">{{ iface }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label>Range Start</label>
|
||||
<input type="text" name="range_start" required placeholder="z.B. 192.168.1.100">
|
||||
</div>
|
||||
<div class="field"><label>Range Ende</label>
|
||||
<input type="text" name="range_end" required placeholder="z.B. 192.168.1.200">
|
||||
<div class="field-hint">Muss im tatsächlich am Interface erkannten Netz liegen — sonst wird das Subnetz abgelehnt.</div>
|
||||
</div>
|
||||
<div class="field"><label>Gateway (optional)</label>
|
||||
<input type="text" name="gateway" placeholder="Automatisch: erkanntes Gateway des Interfaces">
|
||||
</div>
|
||||
<div class="field"><label>DNS-Server (optional)</label>
|
||||
<input type="text" name="dns" placeholder="z.B. 1.1.1.1, 8.8.8.8">
|
||||
<div class="field-hint">Kommagetrennt, falls mehrere.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
|
||||
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Bearbeiten-Modals außerhalb der Karte/Tabelle (siehe Kommentar oben
|
||||
bei den Reservierungen — dieselbe HTML-Validität-Begründung). -->
|
||||
{% if can_edit %}
|
||||
{% for s in subnets %}
|
||||
<div class="modal-overlay" id="editSubnetModal{{ loop.index }}">
|
||||
<div class="modal">
|
||||
<form method="post">
|
||||
<input type="hidden" name="edit_dhcp_subnet" value="{{ s.id }}">
|
||||
<div class="modal-header">
|
||||
<h3>Subnetz bearbeiten</h3>
|
||||
<button type="button" class="modal-close" data-close-modal>×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="field"><label>Interface</label>
|
||||
<select name="interface" required>
|
||||
{% for iface in interfaces %}
|
||||
<option value="{{ iface }}" {% if iface == s.interface %}selected{% endif %}>{{ iface }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label>Range Start</label>
|
||||
<input type="text" name="range_start" value="{{ s.range_start }}" required>
|
||||
</div>
|
||||
<div class="field"><label>Range Ende</label>
|
||||
<input type="text" name="range_end" value="{{ s.range_end }}" required>
|
||||
<div class="field-hint">Muss im tatsächlich am Interface erkannten Netz liegen — sonst wird das Subnetz abgelehnt.</div>
|
||||
</div>
|
||||
<div class="field"><label>Gateway (optional)</label>
|
||||
<input type="text" name="gateway" value="{{ s.gateway or '' }}" placeholder="Automatisch: erkanntes Gateway des Interfaces">
|
||||
</div>
|
||||
<div class="field"><label>DNS-Server (optional)</label>
|
||||
<input type="text" name="dns" value="{{ s.dns or '' }}" placeholder="z.B. 1.1.1.1, 8.8.8.8">
|
||||
<div class="field-hint">Kommagetrennt, falls mehrere.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
|
||||
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user