Version 2.8.11: die Kachel oeffnet den Bildschirm
This commit is contained in:
1 parent
4275c84a6c
commit
0ac651a1b9
9 files changed
+124
-34
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm"
|
name = "tesm"
|
||||||
version = "2.8.10"
|
version = "2.8.11"
|
||||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -39,11 +39,21 @@ def _tiles(*, authenticated: bool) -> dict[str, Any]:
|
|||||||
|
|
||||||
from ..services import inventory, restart
|
from ..services import inventory, restart
|
||||||
|
|
||||||
|
from ..services import vnc as vnc_dienst
|
||||||
|
|
||||||
conn = db()
|
conn = db()
|
||||||
counts = monitor.dashboard_counts(conn)
|
counts = monitor.dashboard_counts(conn)
|
||||||
rows = inventory.list_devices(conn, only_active=not authenticated)
|
rows = inventory.list_devices(conn, only_active=not authenticated)
|
||||||
|
|
||||||
detailed = authenticated and current_user.has("devices.read")
|
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]] = []
|
tiles: list[dict[str, Any]] = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
entry: dict[str, Any] = {
|
entry: dict[str, Any] = {
|
||||||
@@ -74,12 +84,23 @@ def _tiles(*, authenticated: bool) -> dict[str, Any]:
|
|||||||
maintenance_active=_maintenance_active(),
|
maintenance_active=_maintenance_active(),
|
||||||
darf=current_user.has,
|
darf=current_user.has,
|
||||||
),
|
),
|
||||||
# Aus dem letzten Durchlauf, nicht frisch gefragt: die
|
# Die fertige Adresse, nicht nur ein Schalter: der Knopf
|
||||||
# Uebersicht zeigt viele Kacheln und laedt sich selbst
|
# auf der Kachel soll die Verbindung oeffnen und nicht
|
||||||
# nach. Ein Verbindungsversuch je Kachel und Aufbau waere
|
# erst eine Datei ablegen, die jemand anklicken muss.
|
||||||
# genau die Art Hintergrundlast, die niemand bestellt hat.
|
#
|
||||||
"vnc": bool(row["vnc_open"])
|
# Beides -- ob dort ein Server antwortet und wohin gefragt
|
||||||
and current_user.has("device_vnc.execute"),
|
# 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)
|
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)
|
zugang = vnc.zugang(device, port=port, pruefen=online)
|
||||||
if online:
|
if online:
|
||||||
with transaction():
|
with transaction():
|
||||||
monitor.merke_vnc(conn, int(device["id"]), zugang.erreichbar)
|
monitor.merke_vnc(conn, int(device["id"]), zugang.erreichbar, zugang.ziel)
|
||||||
return zugang
|
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]
|
def uebergang_standardgruppen(conn) -> None: # type: ignore[no-untyped-def]
|
||||||
"""Aus sieben Standardgruppen werden drei -- ohne jemandem etwas wegzunehmen.
|
"""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=8, name="funktionsgruppe-fuer-admins", fn=uebergang_funktionsgruppe),
|
||||||
Migration(version=9, name="standardgruppen-aufraeumen", fn=uebergang_standardgruppen),
|
Migration(version=9, name="standardgruppen-aufraeumen", fn=uebergang_standardgruppen),
|
||||||
Migration(version=10, name="vnc-erreichbarkeit", sql=V10),
|
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,
|
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,
|
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.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
|
FROM devices d
|
||||||
LEFT JOIN switches s ON s.id = d.switch_id AND s.deleted_at IS NULL
|
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
|
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
|
duration_ms: int = 0
|
||||||
|
|
||||||
|
|
||||||
def merke_vnc(conn: sqlite3.Connection, device_id: int, offen: bool) -> None:
|
def merke_vnc(
|
||||||
"""Haelt fest, ob dort gerade ein VNC-Server antwortet."""
|
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(
|
conn.execute(
|
||||||
"UPDATE device_status SET vnc_open=?, vnc_checked_at=? WHERE device_id=?",
|
"UPDATE device_status SET vnc_open=?, vnc_checked_at=?, vnc_target=? "
|
||||||
(1 if offen else 0, _now(), device_id),
|
"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)
|
report.duration_ms = int((time.monotonic() - started) * 1000)
|
||||||
return report
|
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"]))
|
ergebnis = ping(str(device["ip"]))
|
||||||
if not vnc_port or not ergebnis.ok:
|
if not vnc_port or not ergebnis.ok:
|
||||||
# Kein Ergebnis heisst "nicht gefragt" -- und laesst den zuletzt
|
# Kein Ergebnis heisst "nicht gefragt" -- und laesst den zuletzt
|
||||||
# bekannten Stand stehen. Ein Geraet, das gerade neu startet, soll
|
# bekannten Stand stehen. Ein Geraet, das gerade neu startet, soll
|
||||||
# seinen Knopf nicht verlieren.
|
# seinen Knopf nicht verlieren.
|
||||||
return device, ergebnis, None
|
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:
|
with ThreadPoolExecutor(max_workers=min(MAX_PARALLEL_CHECKS, len(devices))) as pool:
|
||||||
results = list(pool.map(pruefen, devices))
|
results = list(pool.map(pruefen, devices))
|
||||||
@@ -566,7 +578,7 @@ def run_sweep(
|
|||||||
report.checked += 1
|
report.checked += 1
|
||||||
previous, current = record_status(conn, int(device["id"]), result)
|
previous, current = record_status(conn, int(device["id"]), result)
|
||||||
if vnc_offen is not None:
|
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:
|
if result.ok:
|
||||||
report.online += 1
|
report.online += 1
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -84,6 +84,34 @@
|
|||||||
<a class="btn btn--sm btn--ghost" href="{{ url_for('devices.detail', device_id=tile.id) }}">
|
<a class="btn btn--sm btn--ghost" href="{{ url_for('devices.detail', device_id=tile.id) }}">
|
||||||
Details
|
Details
|
||||||
</a>
|
</a>
|
||||||
|
{% 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>
|
||||||
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" name="active" value="1">
|
||||||
|
<button class="btn btn--sm" type="submit">Aktivieren</button>
|
||||||
|
</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 %}
|
{% if tile.restart_methods %}
|
||||||
{% set method = tile.restart_methods[0] %}
|
{% set method = tile.restart_methods[0] %}
|
||||||
<form method="post" data-no-guard
|
<form method="post" data-no-guard
|
||||||
@@ -99,22 +127,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% 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>
|
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
|
||||||
<input type="hidden" name="active" value="1">
|
|
||||||
<button class="btn btn--sm" type="submit">Aktivieren</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,8 +17,11 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if vnc and vnc.erreichbar %}
|
{% if vnc and vnc.erreichbar %}
|
||||||
<a class="btn btn--sm" href="{{ url_for('devices.vnc_connection', device_id=device.id) }}"
|
{# Wie auf der Statusübersicht: der Knopf öffnet die Verbindung. Die
|
||||||
title="Verbindungsdatei für {{ vnc.ziel }}:{{ vnc.port }} – öffnet den VNC-Viewer">
|
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
|
{{ icon('monitor', size=15) }} VNC
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -237,9 +240,10 @@
|
|||||||
{% if vnc.erreichbar %}
|
{% if vnc.erreichbar %}
|
||||||
<a class="mono" href="{{ vnc.adresse }}">{{ vnc.adresse }}</a>
|
<a class="mono" href="{{ vnc.adresse }}">{{ vnc.adresse }}</a>
|
||||||
<div class="fs-xs faint">
|
<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) }}">
|
<a href="{{ url_for('devices.vnc_connection', device_id=device.id) }}">
|
||||||
Verbindungsdatei herunterladen</a> – enthält nur Ziel und Namen.
|
Verbindungsdatei</a> – sie enthält nur Ziel und Namen.
|
||||||
Der Link daneben braucht einen Viewer, der <code>vnc://</code> übernimmt.
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="faint">
|
<span class="faint">
|
||||||
|
|||||||
@@ -1208,6 +1208,12 @@
|
|||||||
--tile-accent: var(--text-faint);
|
--tile-accent: var(--text-faint);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
/* Spalte, damit die Fusszeile unten haengen kann (siehe .tile__actions).
|
||||||
|
Die Kacheln eines Rasters sind gleich hoch, ihr Inhalt aber nicht: mal
|
||||||
|
fehlt der Switch, mal der Standort. Ohne diesen Halt stuenden die Knoepfe
|
||||||
|
in jeder Kachel auf einer anderen Hoehe -- und das Auge sucht sie. */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
padding: var(--sp-4);
|
padding: var(--sp-4);
|
||||||
padding-left: calc(var(--sp-4) + 3px);
|
padding-left: calc(var(--sp-4) + 3px);
|
||||||
border: 1px solid var(--border-subtle);
|
border: 1px solid var(--border-subtle);
|
||||||
@@ -1269,6 +1275,24 @@
|
|||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fusszeile der Kachel: links der Bildschirm, rechts der Neustart.
|
||||||
|
``margin-top: auto`` schiebt sie an den unteren Rand -- unabhaengig
|
||||||
|
davon, wie viel darueber steht. Die beiden Eingriffe stehen damit in
|
||||||
|
jeder Kachel an derselben Stelle, und der eine liegt nicht dort, wo eben
|
||||||
|
noch der andere war. */
|
||||||
|
.tile__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: var(--sp-3);
|
||||||
|
}
|
||||||
|
.tile__actions > :last-child { margin-left: auto; }
|
||||||
|
/* Steht nur ein Knopf da, bleibt er links -- sonst spraenge der Neustart bei
|
||||||
|
einem Geraet ohne Bildschirmzugang an die Stelle, an der nebenan VNC
|
||||||
|
steht. Das Fuellstueck haelt den Platz. */
|
||||||
|
.tile__spacer { flex: 1 1 auto; }
|
||||||
|
|
||||||
/* Marke und Karte stehen uebereinander im normalen Fluss. Vorher lag die
|
/* Marke und Karte stehen uebereinander im normalen Fluss. Vorher lag die
|
||||||
Marke absolut positioniert hinter der Karte -- auf einem flachen Fenster
|
Marke absolut positioniert hinter der Karte -- auf einem flachen Fenster
|
||||||
schnitt die Karte mitten durch den Schriftzug und die Funktionszeile. Ein
|
schnitt die Karte mitten durch den Schriftzug und die Funktionszeile. Ein
|
||||||
|
|||||||
Reference in new issue
Block a user