Version 2.8.3: Switch-Sitzung wird abgemeldet, Erfolg wird belegt
This commit is contained in:
1 parent
30c2c56656
commit
70e228a2a1
2 files changed
+96
-2
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm"
|
name = "tesm"
|
||||||
version = "2.8.2"
|
version = "2.8.3"
|
||||||
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 = [
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ ERROR_RE = re.compile(
|
|||||||
r"(invalid input|unknown command|incomplete|ambiguous|not supported|% *error)", re.IGNORECASE
|
r"(invalid input|unknown command|incomplete|ambiguous|not supported|% *error)", re.IGNORECASE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#: Der Weg nach draussen -- zweimal "exit" fuehrt aus jedem Modus in den
|
||||||
|
#: privilegierten zurueck, das dritte beendet die Sitzung. Genau so macht es
|
||||||
|
#: das bewaehrte Skript der Vorgaengerfassung.
|
||||||
|
ABMELDUNG = ("exit", "exit", "exit")
|
||||||
|
ABMELDE_PAUSE = 0.15
|
||||||
|
|
||||||
READ_CHUNK = 65536
|
READ_CHUNK = 65536
|
||||||
DEFAULT_COMMAND_TIMEOUT = 20
|
DEFAULT_COMMAND_TIMEOUT = 20
|
||||||
POE_SETTLE_SECONDS = 2
|
POE_SETTLE_SECONDS = 2
|
||||||
@@ -104,7 +110,42 @@ class SwitchCli:
|
|||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
for closer in (self._channel, self._transport):
|
"""Meldet sich ab und legt dann erst auf.
|
||||||
|
|
||||||
|
Der entscheidende Teil, und der einzige, den das ``expect``-Skript der
|
||||||
|
Fassung 1.2.8 hatte und diese Fassung nicht: ``exit`` bis heraus aus
|
||||||
|
dem Konfigurationsmodus, dann ``exit``, und auf die Frage
|
||||||
|
"Do you want to log out (y/n)?" ein ``y``.
|
||||||
|
|
||||||
|
Warum das mehr ist als Hoeflichkeit: ArubaOS erlaubt nur eine Handvoll
|
||||||
|
gleichzeitiger SSH-Sitzungen (``show ip ssh``, Vorgabe fuenf) und
|
||||||
|
raeumt eine weggebrochene Verbindung **nicht sofort** ab. Wer die
|
||||||
|
Verbindung nur fallen laesst, belegt den Platz weiter. Nach ein paar
|
||||||
|
Vorgaengen -- ein Ueberwachungsdurchlauf mit mehreren Geraeten am
|
||||||
|
selben Switch reicht -- sind alle Plaetze belegt. Danach kommt eine
|
||||||
|
neue Sitzung noch durch Anmeldung und Authentisierung, bekommt aber
|
||||||
|
**keine Shell**: kein Prompt, keine Antwort, nichts. Im Betrieb sieht
|
||||||
|
das aus wie ein stummer Switch, und der Portzyklus passiert nicht.
|
||||||
|
|
||||||
|
Fehler werden hier geschluckt: eine Abmeldung, die nicht klappt, darf
|
||||||
|
das Ergebnis des Vorgangs nicht umwerfen -- aufgelegt wird ohnehin.
|
||||||
|
"""
|
||||||
|
kanal = self._channel
|
||||||
|
if kanal is not None:
|
||||||
|
try:
|
||||||
|
for zeile in ABMELDUNG:
|
||||||
|
kanal.send(zeile + "\r\n")
|
||||||
|
# Kurz warten, damit der Switch die Zeile verarbeitet --
|
||||||
|
# auf die Antwort zu warten wuerde beim Abmelden nur
|
||||||
|
# Zeitlimits sammeln.
|
||||||
|
time.sleep(ABMELDE_PAUSE)
|
||||||
|
# "Do you want to log out (y/n)?"
|
||||||
|
kanal.send("y\r\n")
|
||||||
|
time.sleep(ABMELDE_PAUSE)
|
||||||
|
except Exception: # noqa: BLE001 - die Verbindung ist ohnehin am Ende
|
||||||
|
logger.debug("Abmeldung von %s nicht mehr moeglich", self.host)
|
||||||
|
|
||||||
|
for closer in (kanal, self._transport):
|
||||||
try:
|
try:
|
||||||
if closer is not None:
|
if closer is not None:
|
||||||
closer.close()
|
closer.close()
|
||||||
@@ -116,9 +157,26 @@ class SwitchCli:
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
def _read_until_prompt(self, *, timeout: int, initial: bool = False) -> str:
|
def _read_until_prompt(self, *, timeout: int, initial: bool = False) -> str:
|
||||||
|
"""Liest bis zum Prompt. Ohne Prompt ist es ein Fehler, keine Antwort.
|
||||||
|
|
||||||
|
Bis 2.8.2 lief diese Schleife nach dem Zeitlimit einfach aus und gab
|
||||||
|
zurueck, was bis dahin gekommen war -- oft nichts. Der Aufrufer sah
|
||||||
|
eine leere Antwort und machte weiter: ``configure terminal`` ins
|
||||||
|
Nichts, ``no power-over-ethernet`` ins Nichts, und weil die
|
||||||
|
Zustandsabfrage dann "unknown" ergab und "unknown" als
|
||||||
|
"nicht widersprochen" durchging, meldete TESM am Ende **Erfolg** --
|
||||||
|
waehrend am Switch nichts geschaltet wurde. Genau das war im Betrieb
|
||||||
|
zu sehen: der Client blieb durchgehend erreichbar, das Protokoll sagte
|
||||||
|
"durchgefuehrt".
|
||||||
|
|
||||||
|
Ein Kommando ohne Prompt ist ein Kommando ohne Antwort. Das ist ein
|
||||||
|
Fehlschlag und wird als solcher gemeldet -- mit dem, was zuletzt
|
||||||
|
hereinkam, denn daran erkennt man, wo die Sitzung steht.
|
||||||
|
"""
|
||||||
assert self._channel is not None
|
assert self._channel is not None
|
||||||
deadline = time.monotonic() + timeout
|
deadline = time.monotonic() + timeout
|
||||||
collected = b""
|
collected = b""
|
||||||
|
prompt_gesehen = False
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
if self._channel.recv_ready():
|
if self._channel.recv_ready():
|
||||||
chunk = self._channel.recv(READ_CHUNK)
|
chunk = self._channel.recv(READ_CHUNK)
|
||||||
@@ -130,6 +188,7 @@ class SwitchCli:
|
|||||||
collected = PAGER_RE.sub(b"", collected)
|
collected = PAGER_RE.sub(b"", collected)
|
||||||
continue
|
continue
|
||||||
if PROMPT_RE.search(collected[-200:]):
|
if PROMPT_RE.search(collected[-200:]):
|
||||||
|
prompt_gesehen = True
|
||||||
break
|
break
|
||||||
# Der Login-Banner mancher Geraete verlangt eine Bestaetigung.
|
# Der Login-Banner mancher Geraete verlangt eine Bestaetigung.
|
||||||
if initial and collected.rstrip().endswith(b"continue"):
|
if initial and collected.rstrip().endswith(b"continue"):
|
||||||
@@ -138,6 +197,19 @@ class SwitchCli:
|
|||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
text = collected.decode("utf-8", errors="replace")
|
text = collected.decode("utf-8", errors="replace")
|
||||||
self.transcript.append(text)
|
self.transcript.append(text)
|
||||||
|
if not prompt_gesehen:
|
||||||
|
letzte = " ".join(text.split())[-200:]
|
||||||
|
if initial:
|
||||||
|
raise SshError(
|
||||||
|
f"{self.host} hat sich nach der Anmeldung nicht gemeldet "
|
||||||
|
f"(kein Prompt innerhalb von {timeout}s)."
|
||||||
|
+ (f" Zuletzt kam: {letzte}" if letzte else " Es kam nichts.")
|
||||||
|
)
|
||||||
|
raise SshError(
|
||||||
|
f"{self.host} hat auf das Kommando nicht geantwortet "
|
||||||
|
f"(kein Prompt innerhalb von {timeout}s)."
|
||||||
|
+ (f" Zuletzt kam: {letzte}" if letzte else " Es kam nichts.")
|
||||||
|
)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def send(
|
def send(
|
||||||
@@ -212,6 +284,17 @@ class SwitchCli:
|
|||||||
)
|
)
|
||||||
result.transcript = self.full_transcript
|
result.transcript = self.full_transcript
|
||||||
return result
|
return result
|
||||||
|
if before == "enabled" and off_state == "unknown":
|
||||||
|
# Vorher lesbar, nachher nicht: dann ist nicht die Firmware
|
||||||
|
# unbekannt, sondern die Sitzung aus dem Tritt. Das als Erfolg
|
||||||
|
# zu zaehlen war der Fehler, der einen Neustart meldete, den
|
||||||
|
# es nicht gab.
|
||||||
|
result.error = (
|
||||||
|
"Der Zustand des Ports ließ sich nach dem Abschalten nicht mehr "
|
||||||
|
"lesen – vorher ging es. Der Portzyklus ist damit nicht belegt."
|
||||||
|
)
|
||||||
|
result.transcript = self.full_transcript
|
||||||
|
return result
|
||||||
result.add(f"Zustand im ausgeschalteten Zustand: {off_state}")
|
result.add(f"Zustand im ausgeschalteten Zustand: {off_state}")
|
||||||
|
|
||||||
self.set_poe(port, enabled=True)
|
self.set_poe(port, enabled=True)
|
||||||
@@ -224,6 +307,17 @@ class SwitchCli:
|
|||||||
result.transcript = self.full_transcript
|
result.transcript = self.full_transcript
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
if before == "unknown" and off_state == "unknown" and after == "unknown":
|
||||||
|
# Kein einziger Zustand lesbar: die Kommandos hat der Switch
|
||||||
|
# zwar mit einem Prompt bestaetigt (sonst waere die Sitzung
|
||||||
|
# oben abgebrochen), aber belegt ist der Zyklus nicht. Das
|
||||||
|
# gehoert in die Meldung -- "durchgefuehrt" waere zu viel
|
||||||
|
# gesagt.
|
||||||
|
result.add(
|
||||||
|
"Der Zustand war nicht lesbar – die Kommandos hat der Switch "
|
||||||
|
"angenommen, bestätigt ist der Zyklus damit nicht."
|
||||||
|
)
|
||||||
|
|
||||||
result.ok = True
|
result.ok = True
|
||||||
except SshError as exc:
|
except SshError as exc:
|
||||||
result.error = str(exc)
|
result.error = str(exc)
|
||||||
|
|||||||
Reference in new issue
Block a user