diff --git a/README.md b/README.md index 7c4e37e..8b00e43 100644 --- a/README.md +++ b/README.md @@ -265,15 +265,32 @@ der aktiv weiterentwickelte Nachfolger und bildet „globaler Wert, pro Client 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 - Options wie eine Terminal-Boot-URL). Jede Option hat einen **globalen** - Wert sowie optional einen **Wert pro Client** — ein Client-Override - überschreibt den globalen Wert ausschließlich für dieses eine Gerät - (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). +- **Subnetze einzeln deaktivierbar**: ein Subnetz lässt sich ohne Löschen + pausieren (`enabled`-Flag) — wird dann bei „In Datei schreiben“ und + „Aktivieren & (neu) starten“ komplett ignoriert (kein Fehler, bewusst + kein "ungültig", da das Fehlen des Netzes hier Absicht ist), statt es + jedes Mal neu anlegen zu müssen. +- **DHCP-Options** (`dhcp_option_defs`/`dhcp_option_values`): eine kuratierte + Auswahl an **47 Standard-Optionen** (analog den „Vordefinierten Optionen“ + eines Windows-DHCP-Servers — z.B. NTP-/WINS-/TFTP-/SMTP-Server, Boot-Datei, + Domain-Search, IP-Forwarding) ist automatisch vorbefüllt; Router/DNS-Server/ + Domain-Name/Lease-Zeiten sind bewusst ausgenommen, da dafür bereits eigene, + dedizierte Felder existieren (Subnetze bzw. globale Einstellungen oben). + Alle 47 Namen sind live gegen eine echte Kea-2.4.1-Instanz verifiziert + (`kea-dhcp4 -t`, jede Option einzeln getestet). Standard-Optionen bekommen + bewusst **kein eigenes `option-def`** in der generierten Konfiguration — + Kea kennt sie bereits nativ, eine Neudefinition würde die eingebaute + duplizieren; nur zusätzlich angelegte, wirklich eigene/herstellerspezifische + Options (z.B. eine Terminal-Boot-URL) bekommen eines. Beide Arten + funktionieren im UI identisch: Code, Name, Kea-Datentyp und Beschreibung + frei definierbar, jede Option hat einen **globalen** Wert sowie optional + einen **Wert pro Client** — ein Client-Override überschreibt den globalen + Wert ausschließlich für dieses eine Gerät (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). + Ein versehentlich vom Admin gelöschter Standard-Eintrag wird nach einem + Neustart nicht automatisch wieder angelegt (einmalige Migration, kein + Zurückschreiben bei jedem Start). - **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 diff --git a/srv/poe_manager/app.py b/srv/poe_manager/app.py index f5ff19c..64334e2 100644 --- a/srv/poe_manager/app.py +++ b/srv/poe_manager/app.py @@ -491,6 +491,64 @@ def get_db_connection(): return conn +# Kuratierte Standard-DHCPv4-Optionen (analog den "Predefined Options" eines +# Windows-DHCP-Servers) — vorbefüllt in _ensure_schema(), siehe dort für die +# Begründung. Muss vor dem _ensure_schema()-Aufruf am Ende der Datei stehen. +# (code, Kea-Name, Anzeige-Typ, Beschreibung). Router (3)/DNS-Server (6)/ +# Domain-Name (15)/Lease-Zeiten (51/58/59) sind bereits eigene, dedizierte +# Felder und deshalb bewusst nicht enthalten. Alle 47 Namen live gegen eine +# echte Kea-2.4.1-Instanz verifiziert (kea-dhcp4 -t je Option einzeln). +DHCP_STANDARD_OPTIONS = [ + (4, "time-servers", "ipv4-address", "Zeitserver (RFC 868)"), + (7, "log-servers", "ipv4-address", "Log-Server (MIT-LCS UDP)"), + (8, "cookie-servers", "ipv4-address", "Cookie-Server"), + (9, "lpr-servers", "ipv4-address", "LPR-Druckserver"), + (10, "impress-servers", "ipv4-address", "Impress-Server"), + (11, "resource-location-servers", "ipv4-address", "Resource Location Server"), + (16, "swap-server", "ipv4-address", "Swap-Server"), + (17, "root-path", "string", "Root-Pfad (NFS)"), + (18, "extensions-path", "string", "Extensions-Pfad (BOOTP)"), + (19, "ip-forwarding", "boolean", "IP-Forwarding aktivieren"), + (20, "non-local-source-routing", "boolean", "Non-Local Source Routing"), + (26, "interface-mtu", "uint16", "MTU des Interfaces"), + (27, "all-subnets-local", "boolean", "Alle Subnetze lokal erreichbar"), + (28, "broadcast-address", "ipv4-address", "Broadcast-Adresse"), + (29, "perform-mask-discovery", "boolean", "Subnetzmasken-Discovery"), + (30, "mask-supplier", "boolean", "Subnetzmasken-Anbieter"), + (31, "router-discovery", "boolean", "Router Discovery (RFC 1256)"), + (32, "router-solicitation-address", "ipv4-address", "Router-Solicitation-Adresse"), + (33, "static-routes", "string", "Statische Routen (Ziel,Gateway,...)"), + (34, "trailer-encapsulation", "boolean", "Trailer Encapsulation"), + (35, "arp-cache-timeout", "uint32", "ARP-Cache-Timeout (Sek.)"), + (36, "ieee802-3-encapsulation", "boolean", "Ethernet-Encapsulation (IEEE 802.3)"), + (37, "default-tcp-ttl", "uint8", "Standard-TTL für TCP"), + (38, "tcp-keepalive-interval", "uint32", "TCP-Keepalive-Intervall (Sek.)"), + (40, "nis-domain", "string", "NIS-Domain"), + (41, "nis-servers", "ipv4-address", "NIS-Server"), + (42, "ntp-servers", "ipv4-address", "NTP-Server"), + (44, "netbios-name-servers", "ipv4-address", "WINS/NBNS-Server"), + (45, "netbios-dd-server", "ipv4-address", "NetBIOS Datagram Distribution Server"), + (46, "netbios-node-type", "uint8", "WINS/NBT-Knotentyp"), + (47, "netbios-scope", "string", "NetBIOS-Scope-ID"), + (48, "font-servers", "ipv4-address", "X-Window-Font-Server"), + (49, "x-display-manager", "ipv4-address", "X-Window Display Manager"), + (64, "nisplus-domain-name", "string", "NIS+-Domain"), + (65, "nisplus-servers", "ipv4-address", "NIS+-Server"), + (66, "tftp-server-name", "string", "Boot-Server (TFTP-Hostname)"), + (67, "boot-file-name", "string", "Bootdatei-Name"), + (68, "mobile-ip-home-agent", "ipv4-address", "Mobile-IP-Home-Agent"), + (69, "smtp-server", "ipv4-address", "SMTP-Server"), + (70, "pop-server", "ipv4-address", "POP3-Server"), + (71, "nntp-server", "ipv4-address", "NNTP-Server"), + (72, "www-server", "ipv4-address", "WWW-Server"), + (73, "finger-server", "ipv4-address", "Finger-Server"), + (74, "irc-server", "ipv4-address", "IRC-Server"), + (75, "streettalk-server", "ipv4-address", "StreetTalk-Server"), + (76, "streettalk-directory-assistance-server", "ipv4-address", "StreetTalk Directory Assistance Server"), + (119, "domain-search", "string", "DNS-Suchliste (Domain Search)"), +] + + def _ensure_schema(): """Legt neue Tabellen (Gruppen/Rechte) an, falls die DB noch aus einer älteren Version stammt — idempotent, sicher bei jedem Start aufzurufen. @@ -590,9 +648,15 @@ def _ensure_schema(): code INTEGER NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE, type TEXT NOT NULL, - description TEXT + description TEXT, + is_standard INTEGER NOT NULL DEFAULT 0 ) """) + # Migration: "is_standard"-Spalte nachrüsten, falls die Tabelle noch aus + # einer Version ohne vorbefüllte Standard-Optionen stammt. + option_def_cols = {row["name"] for row in conn.execute("PRAGMA table_info(dhcp_option_defs)").fetchall()} + if "is_standard" not in option_def_cols: + conn.execute("ALTER TABLE dhcp_option_defs ADD COLUMN is_standard INTEGER NOT NULL DEFAULT 0") conn.execute(""" CREATE TABLE IF NOT EXISTS dhcp_option_values ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -603,6 +667,26 @@ def _ensure_schema(): UNIQUE (option_def_id, device_mac) ) """) + # Einmalig eine kuratierte Auswahl an Standard-DHCP-Optionen vorbefüllen + # (analog den "Predefined Options" eines Windows-DHCP-Servers) — Router/ + # DNS-Server/Domain-Name/Lease-Zeiten sind bereits eigene, dedizierte + # Felder (siehe dhcp_subnets/DHCP_DEFAULTS) und deshalb hier bewusst + # ausgenommen. Alle Namen live gegen eine echte Kea-2.4.1-Instanz + # verifiziert (kea-dhcp4 -t) — Kea kennt diese Standard-Optionen bereits + # nativ, sie bekommen deshalb (anders als eigene Options) KEIN eigenes + # option-def in der generierten Konfiguration (siehe _render_kea_config), + # sonst würde die eingebaute Definition dupliziert. Per Guard nur + # einmalig, damit ein bewusst vom Admin gelöschter Eintrag nach einem + # Neustart nicht stillschweigend wieder auftaucht. + _migration_key_std_opts = "_seeded_dhcp_standard_options_v1" + if not conn.execute("SELECT 1 FROM settings WHERE key=?", (_migration_key_std_opts,)).fetchone(): + for code, name, opt_type, description in DHCP_STANDARD_OPTIONS: + conn.execute( + "INSERT OR IGNORE INTO dhcp_option_defs (code, name, type, description, is_standard) " + "VALUES (?, ?, ?, ?, 1)", + (code, name, opt_type, description), + ) + conn.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, '1')", (_migration_key_std_opts,)) # DHCP: mehrere gleichzeitige Subnetze statt einer einzelnen globalen # Range — ein Host kann mehrere IPs/Interfaces mit jeweils eigenem Netz @@ -618,9 +702,15 @@ def _ensure_schema(): range_end TEXT NOT NULL, gateway TEXT, dns TEXT, - sort_order INTEGER NOT NULL DEFAULT 0 + sort_order INTEGER NOT NULL DEFAULT 0, + enabled INTEGER NOT NULL DEFAULT 1 ) """) + # Migration: "enabled"-Spalte nachrüsten, falls die Tabelle noch aus + # einer Version ohne Deaktivieren-Funktion stammt. + dhcp_subnet_cols = {row["name"] for row in conn.execute("PRAGMA table_info(dhcp_subnets)").fetchall()} + if "enabled" not in dhcp_subnet_cols: + conn.execute("ALTER TABLE dhcp_subnets ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1") # Migration: bisherige globale Einzel-Range (dhcp_interface/_range_*/ # _gateway/_dns in der settings-Tabelle) einmalig in eine erste Zeile # der neuen Tabelle überführen, statt eine bereits funktionierende @@ -1912,11 +2002,16 @@ def _dhcp_valid_subnets(subnets): """Nimmt die rohen dhcp_subnets-Zeilen und validiert JEDE einzeln gegen ihr tatsächliches Netz (siehe _dhcp_subnet_matches — es muss eine physische IP im gewünschten Bereich vorhanden sein, sonst kann Kea - damit nichts anfangen). Gibt (valid, invalid) zurück: valid ist um - "net_info" angereichert (für Reservierungs-Zuordnung und Kea-Config), - invalid enthält (Zeile, Fehlertext) für die Anzeige/Warnungen.""" + damit nichts anfangen). Deaktivierte Subnetze (enabled=0) werden dabei + komplett übersprungen — bewusst nicht als "invalid" gewertet, da das + Fehlen eines echten Netzes hier kein Fehler ist, sondern Absicht. + Gibt (valid, invalid) zurück: valid ist um "net_info" angereichert (für + Reservierungs-Zuordnung und Kea-Config), invalid enthält (Zeile, + Fehlertext) für die Anzeige/Warnungen.""" valid, invalid = [], [] for s in subnets: + if not s["enabled"]: + continue net_info, error = _dhcp_subnet_matches(s) if error: invalid.append((s, error)) @@ -1975,7 +2070,9 @@ def _dhcp_reservation_candidates(conn, valid_subnets): def _dhcp_option_defs(conn): - return conn.execute("SELECT id, code, name, type, description FROM dhcp_option_defs ORDER BY code ASC").fetchall() + return conn.execute( + "SELECT id, code, name, type, description, is_standard FROM dhcp_option_defs ORDER BY code ASC" + ).fetchall() def _dhcp_option_values(conn): @@ -2045,9 +2142,13 @@ def _render_kea_config(cfg, valid_subnets, reservations_by_subnet, option_defs, Reservierung — Kea überschreibt damit automatisch gleichnamige globale/Subnet-Options für genau diesen Client, ohne dass wir das selbst zusammenrechnen müssen.""" + # Standard-Optionen (is_standard) bekommen KEIN eigenes option-def — Kea + # kennt sie bereits nativ, eine Neudefinition würde die eingebaute + # duplizieren (siehe DHCP_STANDARD_OPTIONS). Nur eigene Options brauchen + # ein option-def, damit Kea Name/Typ/Code überhaupt kennt. option_def_list = [ {"name": d["name"], "code": d["code"], "type": d["type"], "space": "dhcp4"} - for d in option_defs + for d in option_defs if not d["is_standard"] ] global_option_data = [] @@ -2201,6 +2302,20 @@ def settings_dhcp(): log_action("settings.dhcp_subnet_delete", subnet_id) flash("Subnetz gelöscht.", "success") + elif "toggle_dhcp_subnet" in request.form: + subnet_id = request.form.get("toggle_dhcp_subnet") + row = conn.execute("SELECT enabled, interface FROM dhcp_subnets WHERE id=?", (subnet_id,)).fetchone() + if row: + new_state = 0 if row["enabled"] else 1 + conn.execute("UPDATE dhcp_subnets SET enabled=? WHERE id=?", (new_state, subnet_id)) + conn.commit() + log_action("settings.dhcp_subnet_toggle", row["interface"], "aktiviert" if new_state else "deaktiviert") + flash( + "Subnetz aktiviert." if new_state else + "Subnetz deaktiviert — wird beim nächsten Schreiben/Starten nicht mehr berücksichtigt.", + "success", + ) + elif "add_dhcp_option" in request.form: code_raw = request.form.get("code", "").strip() name = request.form.get("name", "").strip() diff --git a/srv/poe_manager/templates/settings_dhcp.html b/srv/poe_manager/templates/settings_dhcp.html index e0d2873..2b72ef3 100644 --- a/srv/poe_manager/templates/settings_dhcp.html +++ b/srv/poe_manager/templates/settings_dhcp.html @@ -113,13 +113,15 @@ {% for s in subnets %} {% set st = subnet_status.get(s.id) %} - + {{ s.interface }} {{ s.range_start }} – {{ s.range_end }} {{ s.gateway or '(automatisch)' }} {{ s.dns or '—' }} - {% if st and st.ok %} + {% if not s.enabled %} + Deaktiviert + {% elif st and st.ok %} Erkannt {{ st.network }} {% else %} @@ -130,6 +132,16 @@ {% if can_edit %}
+
+ + +
@@ -156,8 +168,11 @@
-

Eigene DHCP-Options

-
Herstellerspezifische Options, global oder pro Client überschreibbar.
+

DHCP-Options

+
+ Standard-Optionen (analog den "Predefined Options" eines Windows-DHCP-Servers) sind bereits vorbefüllt, + eigene/herstellerspezifische Options lassen sich zusätzlich anlegen — beide global oder pro Client überschreibbar. +
{% if can_edit %}