diff --git a/apps/tesm/pyproject.toml b/apps/tesm/pyproject.toml index 540910b..8cb6553 100644 --- a/apps/tesm/pyproject.toml +++ b/apps/tesm/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm" -version = "2.8.10" +version = "2.8.11" description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen" requires-python = ">=3.11" dependencies = [ diff --git a/apps/tesm/src/tesm/blueprints/dashboard.py b/apps/tesm/src/tesm/blueprints/dashboard.py index 28efccd..b4304d1 100644 --- a/apps/tesm/src/tesm/blueprints/dashboard.py +++ b/apps/tesm/src/tesm/blueprints/dashboard.py @@ -39,11 +39,21 @@ def _tiles(*, authenticated: bool) -> dict[str, Any]: from ..services import inventory, restart + from ..services import vnc as vnc_dienst + conn = db() counts = monitor.dashboard_counts(conn) rows = inventory.list_devices(conn, only_active=not authenticated) detailed = authenticated and current_user.has("devices.read") + # Einmal je Seitenaufbau, nicht je Kachel: der Port ist fuer alle derselbe, + # und ohne das Recht wird gar nicht erst nachgesehen. + vnc_erlaubt = detailed and current_user.has("device_vnc.execute") + vnc_port = ( + int(core().settings.get(conn, "vnc_port") or vnc_dienst.STANDARD_PORT) + if vnc_erlaubt and core().settings.get(conn, "vnc_enabled") + else 0 + ) tiles: list[dict[str, Any]] = [] for row in rows: entry: dict[str, Any] = { @@ -74,12 +84,23 @@ def _tiles(*, authenticated: bool) -> dict[str, Any]: maintenance_active=_maintenance_active(), darf=current_user.has, ), - # Aus dem letzten Durchlauf, nicht frisch gefragt: die - # Uebersicht zeigt viele Kacheln und laedt sich selbst - # nach. Ein Verbindungsversuch je Kachel und Aufbau waere - # genau die Art Hintergrundlast, die niemand bestellt hat. - "vnc": bool(row["vnc_open"]) - and current_user.has("device_vnc.execute"), + # Die fertige Adresse, nicht nur ein Schalter: der Knopf + # auf der Kachel soll die Verbindung oeffnen und nicht + # erst eine Datei ablegen, die jemand anklicken muss. + # + # Beides -- ob dort ein Server antwortet und wohin gefragt + # wurde -- steht aus dem letzten Durchlauf in der + # Datenbank. Die Uebersicht zeigt viele Kacheln und laedt + # sich selbst nach; ein Verbindungsversuch oder eine + # Namensaufloesung je Kachel und Aufbau waere genau die + # Art Hintergrundlast, die niemand bestellt hat. + "vnc": ( + vnc_dienst.Zugang( + ziel=str(row["vnc_target"]), port=vnc_port, erreichbar=True + ).adresse + if vnc_port and row["vnc_open"] and row["vnc_target"] + else "" + ), } ) tiles.append(entry) diff --git a/apps/tesm/src/tesm/blueprints/devices.py b/apps/tesm/src/tesm/blueprints/devices.py index 8b0f692..e7f7bcd 100644 --- a/apps/tesm/src/tesm/blueprints/devices.py +++ b/apps/tesm/src/tesm/blueprints/devices.py @@ -392,7 +392,7 @@ def _vnc_zugang(conn: Any, device: dict[str, Any]) -> vnc.Zugang | None: zugang = vnc.zugang(device, port=port, pruefen=online) if online: with transaction(): - monitor.merke_vnc(conn, int(device["id"]), zugang.erreichbar) + monitor.merke_vnc(conn, int(device["id"]), zugang.erreichbar, zugang.ziel) return zugang diff --git a/apps/tesm/src/tesm/schema.py b/apps/tesm/src/tesm/schema.py index b2c315a..357dd53 100644 --- a/apps/tesm/src/tesm/schema.py +++ b/apps/tesm/src/tesm/schema.py @@ -503,6 +503,18 @@ ALTER TABLE device_status ADD COLUMN vnc_checked_at TEXT; """ +# Version 11 ---------------------------------------------------------------- +# +# Nicht nur *ob* dort ein VNC-Server antwortet, sondern *wohin* gefragt wurde. +# Die Kacheln der Statusuebersicht verlinken die Verbindung unmittelbar +# (vnc://hap02); das Ziel dafuer aus dem Namen neu herzuleiten hiesse, bei +# jedem Seitenaufbau je Kachel eine Namensaufloesung zu versuchen. Der +# Durchlauf hat sie ohnehin schon gemacht -- also schreibt er sie hin. +V11 = """ +ALTER TABLE device_status ADD COLUMN vnc_target TEXT NOT NULL DEFAULT ''; +""" + + def uebergang_standardgruppen(conn) -> None: # type: ignore[no-untyped-def] """Aus sieben Standardgruppen werden drei -- ohne jemandem etwas wegzunehmen. @@ -546,4 +558,5 @@ TESM_MIGRATIONS: tuple[Migration, ...] = ( Migration(version=8, name="funktionsgruppe-fuer-admins", fn=uebergang_funktionsgruppe), Migration(version=9, name="standardgruppen-aufraeumen", fn=uebergang_standardgruppen), Migration(version=10, name="vnc-erreichbarkeit", sql=V10), + Migration(version=11, name="vnc-ziel", sql=V11), ) diff --git a/apps/tesm/src/tesm/services/inventory.py b/apps/tesm/src/tesm/services/inventory.py index 1311df1..bb4141d 100644 --- a/apps/tesm/src/tesm/services/inventory.py +++ b/apps/tesm/src/tesm/services/inventory.py @@ -542,7 +542,8 @@ def list_devices( WHERE k.host = d.ip AND k.port = d.ssh_port) AS host_key_known, COALESCE(st.state,'unknown') AS state, st.latency_ms, st.last_check_at, st.last_ok_at, st.last_change_at, st.last_restart_at, st.restart_count, - st.fail_count, COALESCE(st.vnc_open, 0) AS vnc_open, st.vnc_checked_at + st.fail_count, COALESCE(st.vnc_open, 0) AS vnc_open, st.vnc_checked_at, + COALESCE(st.vnc_target, '') AS vnc_target FROM devices d LEFT JOIN switches s ON s.id = d.switch_id AND s.deleted_at IS NULL LEFT JOIN credentials c ON c.id = d.credential_id AND c.deleted_at IS NULL diff --git a/apps/tesm/src/tesm/services/monitor.py b/apps/tesm/src/tesm/services/monitor.py index 1727bda..140a6f1 100644 --- a/apps/tesm/src/tesm/services/monitor.py +++ b/apps/tesm/src/tesm/services/monitor.py @@ -507,11 +507,20 @@ class SweepReport: duration_ms: int = 0 -def merke_vnc(conn: sqlite3.Connection, device_id: int, offen: bool) -> None: - """Haelt fest, ob dort gerade ein VNC-Server antwortet.""" +def merke_vnc( + conn: sqlite3.Connection, device_id: int, offen: bool, ziel: str = "" +) -> None: + """Haelt fest, ob dort ein VNC-Server antwortet -- und unter welchem Ziel. + + Das Ziel gehoert dazu, weil die Statusuebersicht die Verbindung unmittelbar + verlinkt. Es aus dem Namen neu herzuleiten hiesse, je Kachel und + Seitenaufbau eine Namensaufloesung zu versuchen; hier steht es einmal + geschrieben, von dem, der ohnehin gefragt hat. + """ conn.execute( - "UPDATE device_status SET vnc_open=?, vnc_checked_at=? WHERE device_id=?", - (1 if offen else 0, _now(), device_id), + "UPDATE device_status SET vnc_open=?, vnc_checked_at=?, vnc_target=? " + "WHERE device_id=?", + (1 if offen else 0, _now(), ziel, device_id), ) @@ -547,14 +556,17 @@ def run_sweep( report.duration_ms = int((time.monotonic() - started) * 1000) return report - def pruefen(device: dict[str, Any]) -> tuple[dict[str, Any], PingResult, bool | None]: + def pruefen( + device: dict[str, Any], + ) -> tuple[dict[str, Any], PingResult, tuple[bool, str] | None]: ergebnis = ping(str(device["ip"])) if not vnc_port or not ergebnis.ok: # Kein Ergebnis heisst "nicht gefragt" -- und laesst den zuletzt # bekannten Stand stehen. Ein Geraet, das gerade neu startet, soll # seinen Knopf nicht verlieren. return device, ergebnis, None - return device, ergebnis, vnc_dienst.erreichbar(vnc_dienst.ziel(device), vnc_port) + ziel = vnc_dienst.ziel(device) + return device, ergebnis, (vnc_dienst.erreichbar(ziel, vnc_port), ziel) with ThreadPoolExecutor(max_workers=min(MAX_PARALLEL_CHECKS, len(devices))) as pool: results = list(pool.map(pruefen, devices)) @@ -566,7 +578,7 @@ def run_sweep( report.checked += 1 previous, current = record_status(conn, int(device["id"]), result) if vnc_offen is not None: - merke_vnc(conn, int(device["id"]), vnc_offen) + merke_vnc(conn, int(device["id"]), vnc_offen[0], vnc_offen[1]) if result.ok: report.online += 1 else: diff --git a/apps/tesm/src/tesm/templates/tesm/_dashboard_tiles.html b/apps/tesm/src/tesm/templates/tesm/_dashboard_tiles.html index 5382c02..c5d53de 100644 --- a/apps/tesm/src/tesm/templates/tesm/_dashboard_tiles.html +++ b/apps/tesm/src/tesm/templates/tesm/_dashboard_tiles.html @@ -84,27 +84,6 @@ Details - {% if tile.restart_methods %} - {% set method = tile.restart_methods[0] %} -
- {% endif %} - {% if tile.vnc %} - - {{ icon('monitor', size=14) }} VNC - - {% endif %} {% if current_user.has('devices.write') and tile.state == 'disabled' %} {% endif %} + {# Die beiden Eingriffe stehen unten und immer an derselben + Stelle: links der Bildschirm, rechts der Neustart. Wer + eine Reihe Kacheln abarbeitet, trifft denselben Knopf, + ohne hinzusehen -- und verwechselt ihn nicht mit dem + anderen. #} + {% if tile.vnc or tile.restart_methods %} +vnc:// übernimmt.
+ Sonst die
- Verbindungsdatei herunterladen – enthält nur Ziel und Namen.
- Der Link daneben braucht einen Viewer, der vnc:// übernimmt.
+ Verbindungsdatei – sie enthält nur Ziel und Namen.