From 34d9cdebd648332720204c2c819cd7cb4ec6b422 Mon Sep 17 00:00:00 2001 From: alientim Date: Thu, 13 Aug 2026 13:00:02 +0200 Subject: [PATCH] =?UTF-8?q?Netzwerkeinstellungen:=20Mehrere=20Interfaces?= =?UTF-8?q?=20korrekt=20unterst=C3=BCtzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Kritischer Bug behoben: _apply_network_config() baute die netplan- Override-Datei bei JEDEM Speichern komplett neu auf (nur mit dem gerade bearbeiteten Interface) statt bestehende Einträge anderer Interfaces zu erhalten -- ein zweites Interface einzurichten hat dadurch die statische Konfiguration des ersten aus der Datei geworfen (fiel auf DHCP zurück). Jetzt wird die vorhandene Datei eingelesen und nur der Block des gespeicherten Interfaces ersetzt/ergänzt. - Live auf POETEST reproduziert und nach dem Fix verifiziert: enp1s0 verlor durch den Bug seine statische 192.168.80.202/24 (fiel auf DHCP zurück), während enp3s0 (zweites, neu eingerichtetes Interface) 192.168.82.202/24 behielt. Nach dem Fix enp1s0 über die echte GUI-Route wiederhergestellt (Werte aus dem aktiven DHCP-Lease bestätigt) -- beide Interfaces stehen jetzt nebeneinander in derselben netplan-Datei. - Neuer JSON-Endpunkt /settings/network_state + JS: die Eingabemaske (Modus/IP/Prefix/Gateway/DNS) lädt beim Umschalten des Interface-Dropdowns jetzt dessen tatsächlichen Ist-Zustand nach, statt weiter die Werte des zuvor angezeigten Interfaces zu zeigen. - Die Übersicht ganz oben auf der Karte zeigt jetzt JEDES aktive Interface gleichzeitig (eigene Zeile mit IP/Gateway/Modus/DNS) statt nur das gerade im Formular ausgewählte. - net_interface-Setting wird jetzt tatsächlich persistiert (war vorbereitet, aber nie geschrieben), damit die Seite nach einem Reload das zuletzt bearbeitete statt immer nur das alphabetisch erste Interface vorbelegt. --- srv/tesm/app.py | 63 ++++++++++++++++++++++++++++++-- srv/tesm/templates/settings.html | 54 ++++++++++++++++++++------- 2 files changed, 100 insertions(+), 17 deletions(-) diff --git a/srv/tesm/app.py b/srv/tesm/app.py index e3b0666..ba54035 100644 --- a/srv/tesm/app.py +++ b/srv/tesm/app.py @@ -2618,7 +2618,23 @@ def _apply_network_config(backend, interface, mode, ip, prefix, gateway, dns_lis eth_cfg["dhcp4-overrides"] = {"use-dns": False} if dns_list: eth_cfg["nameservers"] = {"addresses": dns_list} - doc = {"network": {"version": 2, "ethernets": {interface: eth_cfg}}} + # Bestehende Datei einlesen und nur den Block DIESES Interfaces + # ersetzen/ergänzen -- ein Host kann mehrere Interfaces haben, + # jedes über einen eigenen Aufruf dieser Seite konfiguriert. Ein + # kompletter Neuaufbau des Dokuments (wie ursprünglich) hätte bei + # jedem Speichern alle ANDEREN, bereits konfigurierten Interfaces + # aus der Datei geworfen -- deren Konfiguration fiele dadurch auf + # die Distributor-Datei zurück (i.d.R. DHCP), obwohl an ihnen gar + # nichts geändert wurde (live reproduziert: zweites Interface + # eingerichtet, erstes verlor dadurch seine statische IP). + try: + with open(NETPLAN_CONFIG_PATH, encoding="utf-8") as f: + existing_doc = yaml.safe_load(f) or {} + except OSError: + existing_doc = {} + ethernets = ((existing_doc.get("network") or {}).get("ethernets") or {}).copy() + ethernets[interface] = eth_cfg + doc = {"network": {"version": 2, "ethernets": ethernets}} # Eigene, niedrig priorisierte Datei statt der von cloud-init # mitgelieferten (z.B. 50-cloud-init.yaml) — netplan wendet # Dateien in alphabetischer Reihenfolge an und lässt spätere @@ -2727,6 +2743,11 @@ def settings(): _pending_network_revert.clear() # nur eine ausstehende Änderung gleichzeitig _pending_network_revert[token] = {"timer": timer, "backup": backup} timer.start() + # Damit die Seite beim nächsten Laden das zuletzt bearbeitete + # statt immer nur das alphabetisch erste Interface zeigt -- + # gerade bei mehreren Interfaces sonst verwirrend, siehe + # net_interface-Fallback weiter unten. + set_setting("net_interface", interface) log_action("settings.network_apply", interface, f"Modus {mode}") msg = ( f"Netzwerkkonfiguration angewendet. Falls diese Seite jetzt noch erreichbar ist, bitte " @@ -2826,8 +2847,20 @@ def settings(): return redirect(url_for("settings")) net_backend = _detect_network_backend() - net_interface = get_setting("net_interface") or (_list_network_interfaces() or [None])[0] + all_interfaces = _list_network_interfaces() + net_interface = get_setting("net_interface") or (all_interfaces or [None])[0] net_state = _read_network_state(net_interface, net_backend) if net_interface else None + # Übersicht ganz oben zeigt JEDES tatsächlich aktive Interface (eigene + # IPv4-Adresse vorhanden) gleichzeitig, nicht nur das gerade im + # Formular ausgewählte -- bei mehreren Interfaces sonst leicht der + # Eindruck, das zweite existiere für die App gar nicht (live so + # gemeldet: die Übersicht zeigte nach dem Umschalten der Auswahl + # weiterhin nur die Werte des ersten Interfaces). + active_net_states = [ + {"interface": iface, **_read_network_state(iface, net_backend)} + for iface in all_interfaces + ] + active_net_states = [s for s in active_net_states if s.get("ok")] pending_token = next(iter(_pending_network_revert), None) try: current_hostname = socket.gethostname() @@ -2838,9 +2871,10 @@ def settings(): interval=interval, current_hostname=current_hostname, net_backend=net_backend, - net_interfaces=_list_network_interfaces(), + net_interfaces=all_interfaces, net_interface=net_interface, net_state=net_state, + active_net_states=active_net_states, net_revert_seconds=NETWORK_REVERT_SECONDS, pending_network_token=pending_token, log_rotation_interval=get_setting("log_rotation_interval", LOG_ROTATION_DEFAULT_INTERVAL), @@ -2858,6 +2892,29 @@ def settings(): ) +@app.route("/settings/network_state") +@login_required +def settings_network_state(): + """JSON-Endpunkt für die Interface-Auswahl auf der Netzwerkeinstellungen- + Karte -- liefert den tatsächlichen Ist-Zustand DES ausgewählten + Interfaces, damit die Eingabemaske beim Umschalten live aktualisiert + wird statt weiter die Werte des zuvor angezeigten Interfaces zu zeigen + (bei mehreren Interfaces sonst leicht mit dessen Konfiguration zu + verwechseln -- live reproduziert).""" + if not current_user.can_view_settings_system: + return jsonify({"error": "Keine Berechtigung."}), 403 + interface = request.args.get("interface", "").strip() + if interface not in _list_network_interfaces(): + return jsonify({"error": "Unbekanntes Interface."}), 404 + backend = _detect_network_backend() + state = _read_network_state(interface, backend) + # "network" ist ein ipaddress.IPv4Network-Objekt (siehe + # _detect_interface_network) -- nicht JSON-serialisierbar und vom + # Frontend hier auch nicht benötigt (nur ip/prefix/gateway/dns/mode). + state.pop("network", None) + return jsonify(state) + + @app.route("/settings/import-export") @login_required def settings_import_export(): diff --git a/srv/tesm/templates/settings.html b/srv/tesm/templates/settings.html index 526ba24..f16a93c 100644 --- a/srv/tesm/templates/settings.html +++ b/srv/tesm/templates/settings.html @@ -77,25 +77,29 @@ -
+
{% if net_backend == 'unknown' %} Kein unterstütztes Backend erkannt {% else %} {{ {'networkmanager': 'NetworkManager', 'dhcpcd': 'dhcpcd', 'netplan': 'netplan'}[net_backend] }} {% endif %} - {% if net_state %} +
+ {% for state in active_net_states %} +
- {{ net_interface }} - {% if net_state.ok %}— {{ net_state.ip }}/{{ net_state.prefix }}{% if net_state.gateway %}, Gateway {{ net_state.gateway }}{% endif %}{% endif %} + {{ state.interface }} — {{ state.ip }}/{{ state.prefix }}{% if state.gateway %}, Gateway {{ state.gateway }}{% endif %} - - {{ {'static': 'Statisch', 'dhcp': 'DHCP', 'unknown': 'Modus unbekannt'}[net_state.mode] }} + + {{ {'static': 'Statisch', 'dhcp': 'DHCP', 'unknown': 'Modus unbekannt'}[state.mode] }} + {% if state.dns %} + DNS: {{ state.dns|join(', ') }} {% endif %}
- {% if net_state and net_state.dns %} -
Aktuelle DNS-Server: {{ net_state.dns|join(', ') }}
- {% endif %} + {% else %} +
Kein aktives Interface mit IPv4-Adresse gefunden.
+ {% endfor %} +
{% if pending_network_token %}
@@ -115,11 +119,12 @@
- {% for iface in net_interfaces %} {% endfor %} +
Jedes Interface hat seine eigene Konfiguration — ein Wechsel hier lädt unten dessen tatsächlichen Ist-Zustand, ändert aber noch nichts.
+
- +
- +
- +
Kommagetrennt. Leer lassen, um die per DHCP zugewiesenen DNS-Server zu verwenden.