Version 2.8.25: Wartungsseite ohne Neuladen, Ausweich auf die Adresse
This commit is contained in:
1 parent
9d736bb91f
commit
74d93c4b46
5 files changed
+118
-17
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm"
|
name = "tesm"
|
||||||
version = "2.8.24"
|
version = "2.8.25"
|
||||||
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 = [
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ def status():
|
|||||||
# Geraete zu uebertragen.
|
# Geraete zu uebertragen.
|
||||||
"output": (
|
"output": (
|
||||||
service.letzte_zeilen(str(job["output"] or ""))
|
service.letzte_zeilen(str(job["output"] or ""))
|
||||||
if job["state"] in {"pending", "running"}
|
if service.eben_fertig(dict(job))
|
||||||
else ""
|
else ""
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,30 @@ def _geraet(conn: Any, device_id: int) -> dict[str, Any] | None:
|
|||||||
return device
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
def _erklaerung(fehler: OSError) -> str:
|
||||||
|
"""Die Fehlernummer in einem Satz, der weiterhilft.
|
||||||
|
|
||||||
|
"No route to host" und "Connection refused" sehen im Protokoll gleich
|
||||||
|
harmlos aus und bedeuten das Gegenteil voneinander: einmal kommt man gar
|
||||||
|
nicht hin, einmal kommt man hin und niemand hoert zu. Und beides hat mit
|
||||||
|
einem gelungenen Ping nichts zu tun -- der prueft eine andere Schicht.
|
||||||
|
"""
|
||||||
|
import errno as fehlernummern
|
||||||
|
|
||||||
|
nummer = getattr(fehler, "errno", None)
|
||||||
|
if nummer == fehlernummern.EHOSTUNREACH:
|
||||||
|
return (
|
||||||
|
f"{fehler} – kein Weg dorthin. Eine Firewall oder eine Route steht "
|
||||||
|
"dazwischen; ein gelungener Ping sagt darüber nichts, der prüft eine "
|
||||||
|
"andere Schicht"
|
||||||
|
)
|
||||||
|
if nummer == fehlernummern.ECONNREFUSED:
|
||||||
|
return f"{fehler} – das Gerät ist erreichbar, aber auf diesem Port hört niemand"
|
||||||
|
if nummer == fehlernummern.ETIMEDOUT:
|
||||||
|
return f"{fehler} – keine Antwort; Paketfilter verwerfen meist stillschweigend"
|
||||||
|
return str(fehler)
|
||||||
|
|
||||||
|
|
||||||
def _port(conn: Any) -> int:
|
def _port(conn: Any) -> int:
|
||||||
extension = core()
|
extension = core()
|
||||||
if not extension.settings.get(conn, "vnc_enabled"):
|
if not extension.settings.get(conn, "vnc_enabled"):
|
||||||
@@ -314,10 +338,26 @@ def _strecke(ws, device_id: int) -> None: # type: ignore[no-untyped-def]
|
|||||||
ws.close(1008, "Für diesen Client ist kein Ziel bekannt.")
|
ws.close(1008, "Für diesen Client ist kein Ziel bekannt.")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
gegenstelle = None
|
||||||
gegenstelle = socket.create_connection((host, port), timeout=VERBINDUNGSLIMIT)
|
ausweich = str(device.get("ip") or "").strip()
|
||||||
except OSError as exc:
|
for versuch, ziel_host in enumerate([host] + ([ausweich] if ausweich != host else [])):
|
||||||
ws.close(1011, f"{host}:{port} antwortet nicht ({exc}).")
|
try:
|
||||||
|
gegenstelle = socket.create_connection((ziel_host, port), timeout=VERBINDUNGSLIMIT)
|
||||||
|
if versuch:
|
||||||
|
# Der Name fuehrte ins Leere, die Adresse nicht. Das ist eine
|
||||||
|
# Auskunft ueber das Netz, keine Nebensaechlichkeit: irgendwo
|
||||||
|
# zeigt ein Eintrag woandershin.
|
||||||
|
if live_log := extension.registry.get("live_log"):
|
||||||
|
live_log.warning(
|
||||||
|
"vnc",
|
||||||
|
f"{host} war nicht erreichbar – Sitzung läuft über {ziel_host}",
|
||||||
|
)
|
||||||
|
host = ziel_host
|
||||||
|
break
|
||||||
|
except OSError as exc:
|
||||||
|
letzter = exc
|
||||||
|
if gegenstelle is None:
|
||||||
|
ws.close(1011, f"{host}:{port} antwortet nicht ({_erklaerung(letzter)}).")
|
||||||
return
|
return
|
||||||
# Das Zeitlimit galt dem *Aufbau* -- als Abbruchgrund taugt es nicht.
|
# Das Zeitlimit galt dem *Aufbau* -- als Abbruchgrund taugt es nicht.
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -495,6 +495,11 @@ def run_update(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#: So lange gilt ein Auftrag als "eben fertig geworden" und schickt seine
|
||||||
|
#: letzten Zeilen noch mit. Ohne das verschwaende die Ausgabe genau in dem
|
||||||
|
#: Augenblick, in dem sie interessant wird: wenn der Befehl durch ist.
|
||||||
|
EBEN_FERTIG_SEKUNDEN = 120
|
||||||
|
|
||||||
#: So viele Zeilen gehen ins Mitlesen. Genug, um zu sehen, was gerade
|
#: So viele Zeilen gehen ins Mitlesen. Genug, um zu sehen, was gerade
|
||||||
#: passiert; wenig genug, dass eine Abfrage alle zwei Sekunden nichts kostet.
|
#: passiert; wenig genug, dass eine Abfrage alle zwei Sekunden nichts kostet.
|
||||||
MITLESEN_ZEILEN = 40
|
MITLESEN_ZEILEN = 40
|
||||||
@@ -506,6 +511,22 @@ def letzte_zeilen(ausgabe: str, anzahl: int = MITLESEN_ZEILEN) -> str:
|
|||||||
return "\n".join(zeilen[-anzahl:])
|
return "\n".join(zeilen[-anzahl:])
|
||||||
|
|
||||||
|
|
||||||
|
def eben_fertig(job: dict[str, Any], sekunden: int = EBEN_FERTIG_SEKUNDEN) -> bool:
|
||||||
|
"""Ist dieser Auftrag gerade eben fertig geworden?"""
|
||||||
|
if job.get("state") in {"pending", "running"}:
|
||||||
|
return True
|
||||||
|
stempel = str(job.get("finished_at") or job.get("updated_at") or "")
|
||||||
|
if not stempel:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
moment = datetime.fromisoformat(stempel)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
if moment.tzinfo is None:
|
||||||
|
moment = moment.replace(tzinfo=timezone.utc)
|
||||||
|
return (datetime.now(timezone.utc) - moment).total_seconds() < sekunden
|
||||||
|
|
||||||
|
|
||||||
def befehl_pruefen(befehl: str) -> str:
|
def befehl_pruefen(befehl: str) -> str:
|
||||||
"""Gibt den bereinigten Befehl zurueck -- oder wirft mit Klartext.
|
"""Gibt den bereinigten Befehl zurueck -- oder wirft mit Klartext.
|
||||||
|
|
||||||
|
|||||||
@@ -226,13 +226,13 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
|
|||||||
<thead><tr><th>Gestartet</th><th>Gerät</th><th>Art</th><th>Zustand</th><th>Fortschritt</th><th>Meldung</th></tr></thead>
|
<thead><tr><th>Gestartet</th><th>Gerät</th><th>Art</th><th>Zustand</th><th>Fortschritt</th><th>Meldung</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for job in jobs %}
|
{% for job in jobs %}
|
||||||
<tr>
|
<tr data-auftrag="{{ job.id }}">
|
||||||
<td class="fs-sm nowrap">{{ job.created_at | dt }}</td>
|
<td class="fs-sm nowrap">{{ job.created_at | dt }}</td>
|
||||||
<td class="fs-sm fw-600">
|
<td class="fs-sm fw-600">
|
||||||
<a href="{{ url_for('maintenance.job_detail', job_id=job.id) }}">{{ job.subject }}</a>
|
<a href="{{ url_for('maintenance.job_detail', job_id=job.id) }}">{{ job.subject }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="fs-sm">{{ 'Aktualisierung' if job.kind.endswith('update') else 'Neustart' }}</td>
|
<td class="fs-sm">{{ 'Aktualisierung' if job.kind.endswith('update') else 'Neustart' }}</td>
|
||||||
<td>
|
<td data-auftrag-zustand>
|
||||||
{% if job.state == 'succeeded' %}{{ pill('erfolgreich', 'online') }}
|
{% if job.state == 'succeeded' %}{{ pill('erfolgreich', 'online') }}
|
||||||
{% elif job.state == 'failed' %}{{ pill('fehlgeschlagen', 'danger') }}
|
{% elif job.state == 'failed' %}{{ pill('fehlgeschlagen', 'danger') }}
|
||||||
{% elif job.state == 'running' %}{{ pill('läuft', 'info') }}
|
{% elif job.state == 'running' %}{{ pill('läuft', 'info') }}
|
||||||
@@ -240,11 +240,12 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
|
|||||||
{% else %}{{ pill('wartet', '') }}{% endif %}
|
{% else %}{{ pill('wartet', '') }}{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td style="min-width:120px">
|
<td style="min-width:120px">
|
||||||
<div class="progress {{ 'progress--success' if job.state == 'succeeded' else ('progress--danger' if job.state == 'failed' else '') }}">
|
<div class="progress {{ 'progress--success' if job.state == 'succeeded' else ('progress--danger' if job.state == 'failed' else '') }}"
|
||||||
|
data-auftrag-balken>
|
||||||
<div class="progress__bar" style="width: {{ job.progress }}%"></div>
|
<div class="progress__bar" style="width: {{ job.progress }}%"></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="fs-xs muted">{{ job.message }}</td>
|
<td class="fs-xs muted" data-auftrag-meldung>{{ job.message }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -312,7 +313,42 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
|
|||||||
if (text && zeile.hidden && !offen.has(name)) zeile.hidden = false;
|
if (text && zeile.hidden && !offen.has(name)) zeile.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let liefEtwas = false;
|
// Zustand, Fortschritt und Meldung eines Auftrags nachziehen.
|
||||||
|
//
|
||||||
|
// Vorher lud die Seite sich dafür komplett neu, sobald der letzte Auftrag
|
||||||
|
// durch war -- und riss damit genau das weg, was man lesen wollte: die
|
||||||
|
// Ausgabe stand da, dann sprang die Seite, und danach war sie leer.
|
||||||
|
const ZUSTAENDE = {
|
||||||
|
succeeded: ["erfolgreich", "pill--online", "progress--success"],
|
||||||
|
failed: ["fehlgeschlagen", "pill--danger", "progress--danger"],
|
||||||
|
running: ["läuft", "pill--info", ""],
|
||||||
|
unknown: ["unklar", "pill--warning", ""],
|
||||||
|
pending: ["wartet", "", ""],
|
||||||
|
};
|
||||||
|
|
||||||
|
function auftragZeigen(job) {
|
||||||
|
const zeile = document.querySelector(`[data-auftrag="${CSS.escape(job.id)}"]`);
|
||||||
|
if (!zeile) return false;
|
||||||
|
const [text, pillenKlasse, balkenKlasse] = ZUSTAENDE[job.state] || ZUSTAENDE.pending;
|
||||||
|
const pille = zeile.querySelector("[data-auftrag-zustand] .pill");
|
||||||
|
if (pille && pille.textContent.trim() !== text) {
|
||||||
|
pille.textContent = text;
|
||||||
|
pille.className = `pill ${pillenKlasse}`.trim();
|
||||||
|
}
|
||||||
|
const balken = zeile.querySelector("[data-auftrag-balken]");
|
||||||
|
if (balken) {
|
||||||
|
balken.className = `progress ${balkenKlasse}`.trim();
|
||||||
|
const inner = balken.querySelector(".progress__bar");
|
||||||
|
if (inner) inner.style.width = `${job.progress || 0}%`;
|
||||||
|
}
|
||||||
|
const meldung = zeile.querySelector("[data-auftrag-meldung]");
|
||||||
|
if (meldung && meldung.textContent.trim() !== (job.message || "")) {
|
||||||
|
meldung.textContent = job.message || "";
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let neueAuftraege = false;
|
||||||
async function poll() {
|
async function poll() {
|
||||||
let weiter = 10000;
|
let weiter = 10000;
|
||||||
try {
|
try {
|
||||||
@@ -320,6 +356,10 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
|
|||||||
weiter = state.poll_ms || 10000;
|
weiter = state.poll_ms || 10000;
|
||||||
state.jobs.forEach((job) => {
|
state.jobs.forEach((job) => {
|
||||||
if (job.output) zeigen(job.subject, job.output);
|
if (job.output) zeigen(job.subject, job.output);
|
||||||
|
// Ein Auftrag, der in der Tabelle noch gar nicht steht, ist neu --
|
||||||
|
// dafür braucht es die Seite einmal frisch, aber erst, wenn nichts
|
||||||
|
// mehr läuft.
|
||||||
|
if (!auftragZeigen(job)) neueAuftraege = true;
|
||||||
});
|
});
|
||||||
// Erreichbarkeit mitziehen -- sonst steht die Seite mit dem Stand ihres
|
// Erreichbarkeit mitziehen -- sonst steht die Seite mit dem Stand ihres
|
||||||
// Aufbaus da, und wer nach einem Neustart auf die Rückkehr wartet, sieht
|
// Aufbaus da, und wer nach einem Neustart auf die Rückkehr wartet, sieht
|
||||||
@@ -339,13 +379,13 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
|
|||||||
pille.className = `pill ${art}`;
|
pille.className = `pill ${art}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Erst wenn der letzte Auftrag durch ist, einmal neu laden: dann stimmt
|
// Neu laden nur, wenn Aufträge dazugekommen sind, die in der Tabelle
|
||||||
// auch die Auftragsliste daneben.
|
// fehlen -- und auch dann erst, wenn nichts mehr läuft. Wer zusieht,
|
||||||
if (liefEtwas && !state.running) {
|
// soll nicht mitten im Lesen weggerissen werden.
|
||||||
location.reload();
|
if (neueAuftraege && !state.running) {
|
||||||
return;
|
window.setTimeout(() => location.reload(), 8000);
|
||||||
|
neueAuftraege = false;
|
||||||
}
|
}
|
||||||
liefEtwas = state.running;
|
|
||||||
} catch {
|
} catch {
|
||||||
/* stumm bleiben – die Seite ist auch ohne Aktualisierung nutzbar */
|
/* stumm bleiben – die Seite ist auch ohne Aktualisierung nutzbar */
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user