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 @@ -