Version 2.8.11: die Kachel oeffnet den Bildschirm
This commit is contained in:
1 parent
4275c84a6c
commit
0ac651a1b9
9 files changed
+131
-41
No files matched your search
@@ -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 = [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -84,27 +84,6 @@
|
||||
<a class="btn btn--sm btn--ghost" href="{{ url_for('devices.detail', device_id=tile.id) }}">
|
||||
Details
|
||||
</a>
|
||||
{% if tile.restart_methods %}
|
||||
{% set method = tile.restart_methods[0] %}
|
||||
<form method="post" data-no-guard
|
||||
action="{{ url_for('devices.restart_device', device_id=tile.id) }}">
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="method" value="{{ method.key }}">
|
||||
<button class="btn btn--sm" type="submit" title="{{ method.hint }}"
|
||||
data-busy-label="Startet ..."
|
||||
data-confirm="{{ tile.name }} über {{ method.label }} neu starten? {{ method.hint }}"
|
||||
data-confirm-title="Neustart" data-confirm-ok="Neu starten"
|
||||
data-confirm-style="primary">
|
||||
{{ icon('power', size=14) }} {{ method.label }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if tile.vnc %}
|
||||
<a class="btn btn--sm" title="Verbindungsdatei für den Bildschirm"
|
||||
href="{{ url_for('devices.vnc_connection', device_id=tile.id) }}">
|
||||
{{ icon('monitor', size=14) }} VNC
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if current_user.has('devices.write') and tile.state == 'disabled' %}
|
||||
<form method="post" action="{{ url_for('devices.toggle_active', device_id=tile.id) }}"
|
||||
data-no-guard>
|
||||
@@ -114,6 +93,42 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{# 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 %}
|
||||
<div class="tile__actions">
|
||||
{% if tile.vnc %}
|
||||
{# Unmittelbar die Verbindung, keine Datei: auf der
|
||||
Uebersicht will man hin, nicht etwas herunterladen.
|
||||
Die Verbindungsdatei fuer Viewer ohne registriertes
|
||||
Schema steht auf der Detailseite. #}
|
||||
<a class="btn btn--sm" href="{{ tile.vnc }}"
|
||||
title="Bildschirm öffnen ({{ tile.vnc }})">
|
||||
{{ icon('monitor', size=14) }} VNC
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="tile__spacer" aria-hidden="true"></span>
|
||||
{% endif %}
|
||||
{% if tile.restart_methods %}
|
||||
{% set method = tile.restart_methods[0] %}
|
||||
<form method="post" data-no-guard
|
||||
action="{{ url_for('devices.restart_device', device_id=tile.id) }}">
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="method" value="{{ method.key }}">
|
||||
<button class="btn btn--sm" type="submit" title="{{ method.hint }}"
|
||||
data-busy-label="Startet ..."
|
||||
data-confirm="{{ tile.name }} über {{ method.label }} neu starten? {{ method.hint }}"
|
||||
data-confirm-title="Neustart" data-confirm-ok="Neu starten"
|
||||
data-confirm-style="primary">
|
||||
{{ icon('power', size=14) }} {{ method.label }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if vnc and vnc.erreichbar %}
|
||||
<a class="btn btn--sm" href="{{ url_for('devices.vnc_connection', device_id=device.id) }}"
|
||||
title="Verbindungsdatei für {{ vnc.ziel }}:{{ vnc.port }} – öffnet den VNC-Viewer">
|
||||
{# Wie auf der Statusübersicht: der Knopf öffnet die Verbindung. Die
|
||||
Verbindungsdatei steht eine Zeile tiefer in den Stammdaten -- für
|
||||
Arbeitsplätze, an denen kein Viewer das Schema übernimmt. #}
|
||||
<a class="btn btn--sm" href="{{ vnc.adresse }}"
|
||||
title="Bildschirm öffnen ({{ vnc.adresse }})">
|
||||
{{ icon('monitor', size=15) }} VNC
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -237,9 +240,10 @@
|
||||
{% if vnc.erreichbar %}
|
||||
<a class="mono" href="{{ vnc.adresse }}">{{ vnc.adresse }}</a>
|
||||
<div class="fs-xs faint">
|
||||
Öffnet den Viewer unmittelbar, sofern er <code>vnc://</code> übernimmt.
|
||||
Sonst die
|
||||
<a href="{{ url_for('devices.vnc_connection', device_id=device.id) }}">
|
||||
Verbindungsdatei herunterladen</a> – enthält nur Ziel und Namen.
|
||||
Der Link daneben braucht einen Viewer, der <code>vnc://</code> übernimmt.
|
||||
Verbindungsdatei</a> – sie enthält nur Ziel und Namen.
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="faint">
|
||||
|
||||
Reference in new issue
Block a user