Netzwerkeinstellungen: Mehrere Interfaces korrekt unterstützen
- 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.
This commit is contained in:
+60
-3
@@ -2618,7 +2618,23 @@ def _apply_network_config(backend, interface, mode, ip, prefix, gateway, dns_lis
|
|||||||
eth_cfg["dhcp4-overrides"] = {"use-dns": False}
|
eth_cfg["dhcp4-overrides"] = {"use-dns": False}
|
||||||
if dns_list:
|
if dns_list:
|
||||||
eth_cfg["nameservers"] = {"addresses": 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
|
# Eigene, niedrig priorisierte Datei statt der von cloud-init
|
||||||
# mitgelieferten (z.B. 50-cloud-init.yaml) — netplan wendet
|
# mitgelieferten (z.B. 50-cloud-init.yaml) — netplan wendet
|
||||||
# Dateien in alphabetischer Reihenfolge an und lässt spätere
|
# 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.clear() # nur eine ausstehende Änderung gleichzeitig
|
||||||
_pending_network_revert[token] = {"timer": timer, "backup": backup}
|
_pending_network_revert[token] = {"timer": timer, "backup": backup}
|
||||||
timer.start()
|
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}")
|
log_action("settings.network_apply", interface, f"Modus {mode}")
|
||||||
msg = (
|
msg = (
|
||||||
f"Netzwerkkonfiguration angewendet. Falls diese Seite jetzt noch erreichbar ist, bitte "
|
f"Netzwerkkonfiguration angewendet. Falls diese Seite jetzt noch erreichbar ist, bitte "
|
||||||
@@ -2826,8 +2847,20 @@ def settings():
|
|||||||
return redirect(url_for("settings"))
|
return redirect(url_for("settings"))
|
||||||
|
|
||||||
net_backend = _detect_network_backend()
|
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
|
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)
|
pending_token = next(iter(_pending_network_revert), None)
|
||||||
try:
|
try:
|
||||||
current_hostname = socket.gethostname()
|
current_hostname = socket.gethostname()
|
||||||
@@ -2838,9 +2871,10 @@ def settings():
|
|||||||
interval=interval,
|
interval=interval,
|
||||||
current_hostname=current_hostname,
|
current_hostname=current_hostname,
|
||||||
net_backend=net_backend,
|
net_backend=net_backend,
|
||||||
net_interfaces=_list_network_interfaces(),
|
net_interfaces=all_interfaces,
|
||||||
net_interface=net_interface,
|
net_interface=net_interface,
|
||||||
net_state=net_state,
|
net_state=net_state,
|
||||||
|
active_net_states=active_net_states,
|
||||||
net_revert_seconds=NETWORK_REVERT_SECONDS,
|
net_revert_seconds=NETWORK_REVERT_SECONDS,
|
||||||
pending_network_token=pending_token,
|
pending_network_token=pending_token,
|
||||||
log_rotation_interval=get_setting("log_rotation_interval", LOG_ROTATION_DEFAULT_INTERVAL),
|
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")
|
@app.route("/settings/import-export")
|
||||||
@login_required
|
@login_required
|
||||||
def settings_import_export():
|
def settings_import_export():
|
||||||
|
|||||||
@@ -77,25 +77,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2" style="align-items:center; margin-bottom:16px; flex-wrap:wrap;">
|
<div class="flex gap-2" style="align-items:center; margin-bottom:12px; flex-wrap:wrap;">
|
||||||
{% if net_backend == 'unknown' %}
|
{% if net_backend == 'unknown' %}
|
||||||
<span class="pill unknown">Kein unterstütztes Backend erkannt</span>
|
<span class="pill unknown">Kein unterstütztes Backend erkannt</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="pill online">{{ {'networkmanager': 'NetworkManager', 'dhcpcd': 'dhcpcd', 'netplan': 'netplan'}[net_backend] }}</span>
|
<span class="pill online">{{ {'networkmanager': 'NetworkManager', 'dhcpcd': 'dhcpcd', 'netplan': 'netplan'}[net_backend] }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if net_state %}
|
</div>
|
||||||
|
{% for state in active_net_states %}
|
||||||
|
<div class="flex gap-2" style="align-items:center; margin-bottom:8px; flex-wrap:wrap;">
|
||||||
<span class="mono text-faint" style="font-size:12px;">
|
<span class="mono text-faint" style="font-size:12px;">
|
||||||
{{ net_interface }}
|
{{ state.interface }} — {{ state.ip }}/{{ state.prefix }}{% if state.gateway %}, Gateway {{ state.gateway }}{% endif %}
|
||||||
{% if net_state.ok %}— {{ net_state.ip }}/{{ net_state.prefix }}{% if net_state.gateway %}, Gateway {{ net_state.gateway }}{% endif %}{% endif %}
|
|
||||||
</span>
|
</span>
|
||||||
<span class="pill {{ 'user' if net_state.mode == 'static' else ('online' if net_state.mode == 'dhcp' else 'unknown') }}">
|
<span class="pill {{ 'user' if state.mode == 'static' else ('online' if state.mode == 'dhcp' else 'unknown') }}">
|
||||||
{{ {'static': 'Statisch', 'dhcp': 'DHCP', 'unknown': 'Modus unbekannt'}[net_state.mode] }}
|
{{ {'static': 'Statisch', 'dhcp': 'DHCP', 'unknown': 'Modus unbekannt'}[state.mode] }}
|
||||||
</span>
|
</span>
|
||||||
|
{% if state.dns %}
|
||||||
|
<span class="text-faint" style="font-size:12px;">DNS: <span class="mono">{{ state.dns|join(', ') }}</span></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if net_state and net_state.dns %}
|
{% else %}
|
||||||
<div class="text-faint" style="font-size:12px; margin-bottom:16px;">Aktuelle DNS-Server: <span class="mono">{{ net_state.dns|join(', ') }}</span></div>
|
<div class="text-faint" style="font-size:12px; margin-bottom:8px;">Kein aktives Interface mit IPv4-Adresse gefunden.</div>
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
|
<div style="margin-bottom:16px;"></div>
|
||||||
|
|
||||||
{% if pending_network_token %}
|
{% if pending_network_token %}
|
||||||
<div class="card-pad" style="background:var(--warning-dim); border-radius:var(--radius-sm); margin-bottom:16px;">
|
<div class="card-pad" style="background:var(--warning-dim); border-radius:var(--radius-sm); margin-bottom:16px;">
|
||||||
@@ -115,11 +119,12 @@
|
|||||||
<form method="post" data-confirm="Netzwerkkonfiguration wirklich ändern? Falls die Verbindung danach abbricht, wird die vorherige Konfiguration automatisch nach {{ net_revert_seconds }} Sekunden wiederhergestellt.">
|
<form method="post" data-confirm="Netzwerkkonfiguration wirklich ändern? Falls die Verbindung danach abbricht, wird die vorherige Konfiguration automatisch nach {{ net_revert_seconds }} Sekunden wiederhergestellt.">
|
||||||
<input type="hidden" name="apply_network" value="1">
|
<input type="hidden" name="apply_network" value="1">
|
||||||
<div class="field"><label>Interface</label>
|
<div class="field"><label>Interface</label>
|
||||||
<select name="net_interface">
|
<select name="net_interface" id="netInterfaceSelect" onchange="loadNetworkState(this.value)">
|
||||||
{% for iface in net_interfaces %}
|
{% for iface in net_interfaces %}
|
||||||
<option value="{{ iface }}" {% if iface == net_interface %}selected{% endif %}>{{ iface }}</option>
|
<option value="{{ iface }}" {% if iface == net_interface %}selected{% endif %}>{{ iface }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
<div class="field-hint">Jedes Interface hat seine eigene Konfiguration — ein Wechsel hier lädt unten dessen tatsächlichen Ist-Zustand, ändert aber noch nichts.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field"><label>Modus</label>
|
<div class="field"><label>Modus</label>
|
||||||
<select name="net_mode" id="netModeSelect" onchange="document.getElementById('netStaticFields').classList.toggle('hidden', this.value !== 'static')">
|
<select name="net_mode" id="netModeSelect" onchange="document.getElementById('netStaticFields').classList.toggle('hidden', this.value !== 'static')">
|
||||||
@@ -129,17 +134,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="netStaticFields" class="{{ 'hidden' if net_state.mode != 'static' }}">
|
<div id="netStaticFields" class="{{ 'hidden' if net_state.mode != 'static' }}">
|
||||||
<div class="field"><label>IP-Adresse</label>
|
<div class="field"><label>IP-Adresse</label>
|
||||||
<input type="text" name="net_ip" value="{{ net_state.ip if net_state.mode == 'static' else '' }}" placeholder="z.B. 192.168.1.50">
|
<input type="text" name="net_ip" id="netIpInput" value="{{ net_state.ip if net_state.mode == 'static' else '' }}" placeholder="z.B. 192.168.1.50">
|
||||||
</div>
|
</div>
|
||||||
<div class="field"><label>Prefix (CIDR-Bits)</label>
|
<div class="field"><label>Prefix (CIDR-Bits)</label>
|
||||||
<input type="number" name="net_prefix" min="1" max="32" value="{{ net_state.prefix if net_state.mode == 'static' else '' }}" placeholder="z.B. 24">
|
<input type="number" name="net_prefix" id="netPrefixInput" min="1" max="32" value="{{ net_state.prefix if net_state.mode == 'static' else '' }}" placeholder="z.B. 24">
|
||||||
</div>
|
</div>
|
||||||
<div class="field"><label>Gateway</label>
|
<div class="field"><label>Gateway</label>
|
||||||
<input type="text" name="net_gateway" value="{{ net_state.gateway if net_state.mode == 'static' else '' }}" placeholder="z.B. 192.168.1.1">
|
<input type="text" name="net_gateway" id="netGatewayInput" value="{{ net_state.gateway if net_state.mode == 'static' else '' }}" placeholder="z.B. 192.168.1.1">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field"><label>DNS-Server</label>
|
<div class="field"><label>DNS-Server</label>
|
||||||
<input type="text" name="net_dns" value="{{ net_state.dns|join(', ') if net_state and net_state.dns else '' }}" placeholder="z.B. 1.1.1.1, 8.8.8.8">
|
<input type="text" name="net_dns" id="netDnsInput" value="{{ net_state.dns|join(', ') if net_state and net_state.dns else '' }}" placeholder="z.B. 1.1.1.1, 8.8.8.8">
|
||||||
<div class="field-hint">Kommagetrennt. Leer lassen, um die per DHCP zugewiesenen DNS-Server zu verwenden.</div>
|
<div class="field-hint">Kommagetrennt. Leer lassen, um die per DHCP zugewiesenen DNS-Server zu verwenden.</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary btn-block">
|
<button type="submit" class="btn btn-primary btn-block">
|
||||||
@@ -302,5 +307,26 @@ function moveNavItem(btn, dir) {
|
|||||||
if (dir === -1) li.parentNode.insertBefore(li, target);
|
if (dir === -1) li.parentNode.insertBefore(li, target);
|
||||||
else li.parentNode.insertBefore(target, li);
|
else li.parentNode.insertBefore(target, li);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lädt beim Umschalten des Interfaces (Netzwerkeinstellungen) dessen
|
||||||
|
// tatsächlichen Ist-Zustand nach -- ohne das würden die Felder weiter die
|
||||||
|
// Werte des zuvor angezeigten Interfaces zeigen, obwohl gerade ein anderes
|
||||||
|
// ausgewählt wurde (bei mehreren Interfaces leicht zu Verwechslungen
|
||||||
|
// geführt, siehe /settings/network_state).
|
||||||
|
function loadNetworkState(iface) {
|
||||||
|
fetch("{{ url_for('settings_network_state') }}?interface=" + encodeURIComponent(iface))
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(state => {
|
||||||
|
if (state.error) return;
|
||||||
|
const isStatic = state.mode === "static";
|
||||||
|
document.getElementById("netModeSelect").value = isStatic ? "static" : "dhcp";
|
||||||
|
document.getElementById("netStaticFields").classList.toggle("hidden", !isStatic);
|
||||||
|
document.getElementById("netIpInput").value = isStatic ? (state.ip || "") : "";
|
||||||
|
document.getElementById("netPrefixInput").value = isStatic ? (state.prefix || "") : "";
|
||||||
|
document.getElementById("netGatewayInput").value = isStatic ? (state.gateway || "") : "";
|
||||||
|
document.getElementById("netDnsInput").value = (state.dns || []).join(", ");
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user