src/voxip_kiosk -> src/daynivo_kiosk, with every import, the launchers, install.sh, the Makefile and the manager's gunicorn app path. An upgrade replaces /usr/lib/voxip-kiosk/python as a whole, so the old package disappears; tested by upgrading an installation of the previous commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
128 lines
4.8 KiB
Python
128 lines
4.8 KiB
Python
"""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
|