"""Network status for the web interface (read-only, no privileges needed).""" from __future__ import annotations import json import re import shutil import subprocess from pathlib import Path from typing import Any from .netconfig import INTERFACES, profile_name, split_terse STATE_LABELS = { "connected": "Verbonden", "connecting": "Bezig met verbinden", "disconnected": "Niet verbonden", "unavailable": "Niet beschikbaar", "unmanaged": "Niet beheerd", } def _nmcli(*args: str) -> str | None: if not shutil.which("nmcli"): return None try: proc = subprocess.run(["nmcli", *args], capture_output=True, text=True, timeout=10) except (OSError, subprocess.SubprocessError): return None return proc.stdout if proc.returncode == 0 else None def device_details(device: str) -> dict[str, Any]: out = _nmcli("-t", "device", "show", device) info: dict[str, Any] = {"addresses": [], "dns": [], "gateway": "", "hwaddr": "", "state": "", "connection": ""} if not out: return info for line in out.splitlines(): key, _, value = line.partition(":") value = value.replace("\\:", ":") base = key.split("[", 1)[0] if base == "IP4.ADDRESS" and value: info["addresses"].append(value) elif base == "IP4.DNS" and value: info["dns"].append(value) elif base == "IP4.GATEWAY" and value and value != "--": info["gateway"] = value elif base == "GENERAL.HWADDR": info["hwaddr"] = value elif base == "GENERAL.STATE": info["state"] = re.sub(r"^\d+\s*", "", value).strip("() ") elif base == "GENERAL.CONNECTION" and value != "--": info["connection"] = value return info def profile_settings(interface: str) -> dict[str, Any]: """Current settings of our own profile, used to pre-fill the forms.""" out = _nmcli("-t", "-f", "ipv4.method,ipv4.addresses,ipv4.gateway,ipv4.dns,802-11-wireless.ssid,802-11-wireless.hidden", "connection", "show", profile_name(interface)) settings = {"exists": False, "method": "dhcp", "address": "", "gateway": "", "dns": "", "ssid": "", "hidden": False} if not out: return settings settings["exists"] = True for line in out.splitlines(): key, _, value = line.partition(":") value = value.replace("\\:", ":") if value == "--": value = "" if key == "ipv4.method": settings["method"] = "static" if value == "manual" else "dhcp" elif key == "ipv4.addresses": settings["address"] = value.split(",")[0].strip() elif key == "ipv4.gateway": settings["gateway"] = value elif key == "ipv4.dns": settings["dns"] = ", ".join(x for x in value.split(",") if x) elif key == "802-11-wireless.ssid": settings["ssid"] = value elif key == "802-11-wireless.hidden": settings["hidden"] = value == "yes" return settings def overview() -> dict[str, Any]: available = shutil.which("nmcli") is not None devices = {} status = _nmcli("-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status") or "" present = {split_terse(line)[0]: split_terse(line) for line in status.splitlines() if line} for iface, kind in INTERFACES.items(): fields = present.get(iface) details = device_details(iface) if fields else {} state = fields[2] if fields and len(fields) > 2 else "" devices[iface] = { "interface": iface, "kind": kind, "present": fields is not None, "state": state, "state_label": STATE_LABELS.get(state, state or "Niet aanwezig"), "connected": state == "connected", "connection": fields[3] if fields and len(fields) > 3 and fields[3] != "--" else "", "addresses": details.get("addresses", []), "gateway": details.get("gateway", ""), "dns": details.get("dns", []), "hwaddr": details.get("hwaddr", ""), "profile": profile_settings(iface) if available else {}, } return {"available": available, "devices": devices, "wifi_country": wifi_country()} def wifi_country() -> str: if not shutil.which("iw"): return "" try: out = subprocess.run(["iw", "reg", "get"], capture_output=True, text=True, timeout=5).stdout except (OSError, subprocess.SubprocessError): return "" match = re.search(r"country ([A-Z0-9]{2}):", out) return match.group(1) if match and match.group(1) != "00" else "" def pending_change(pending_file: Path) -> dict[str, Any] | None: try: data = json.loads(pending_file.read_text(encoding="utf-8")) except (OSError, ValueError): return None return data if isinstance(data, dict) else None