Files
server-manager/core/telnet_client.py
chrome-storm-c442 eede67e6a9 feat: multi-type server support — SQL, Redis, Grafana, Prometheus, Telnet, WinRM, RDP/VNC
Full implementation of multi-type server management across GUI and CLI:

New clients: SQLClient (MariaDB/MSSQL/PostgreSQL), RedisClient, GrafanaClient,
PrometheusClient, TelnetSession, WinRMClient, RemoteDesktopLauncher.

New GUI tabs: QueryTab (SQL editor + Treeview), RedisTab (console + history),
GrafanaTab (dashboards + alerts), PrometheusTab (PromQL + targets),
PowershellTab (PS/CMD), LaunchTab (RDP/VNC external client).

Infrastructure: TAB_REGISTRY for conditional tabs per server type,
adaptive server_dialog fields, colored type badges in sidebar,
status checker for all types (SSH/TCP/SQL/Redis/HTTP), 100+ i18n keys.

CLI: ssh.py extended with --sql, --redis, --grafana-*, --prom-*, --ps, --cmd.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 09:35:24 -05:00

181 lines
6.2 KiB
Python

"""
Telnet client — interactive telnet session with the same interface as ShellSession.
"""
import asyncio
import threading
from core.logger import log
class TelnetSession:
"""Interactive telnet session — same interface as ShellSession from ssh_client.py."""
def __init__(self, server: dict, cols: int = 80, rows: int = 24):
self.server = server
self.cols = cols
self.rows = rows
self._loop: asyncio.AbstractEventLoop | None = None
self._thread: threading.Thread | None = None
self._reader = None
self._writer = None
self._running = False
# Callbacks — set by the owner
self.on_data = None # on_data(data: bytes)
self.on_disconnect = None # on_disconnect()
@property
def connected(self) -> bool:
return self._running and self._writer is not None
def connect(self):
"""Start telnet connection in a background thread running an asyncio event loop."""
self._running = True
self._thread = threading.Thread(target=self._run_loop, daemon=True)
self._thread.start()
def _run_loop(self):
"""Entry point for the background thread — creates event loop and runs connection."""
self._loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._loop)
try:
self._loop.run_until_complete(self._async_connect())
except Exception as e:
log.debug(f"TelnetSession loop error: {e}")
finally:
self._running = False
try:
self._loop.close()
except Exception:
pass
self._loop = None
if self.on_disconnect:
self.on_disconnect()
async def _async_connect(self):
"""Async telnet connection: open, login, then read loop."""
try:
import telnetlib3
except ImportError:
log.error("telnetlib3 not installed. Run: pip install telnetlib3")
raise ImportError("telnetlib3 is required for telnet connections")
hostname = self.server["ip"]
port = self.server.get("port", 23)
user = self.server.get("user", "")
password = self.server.get("password", "")
log.info(f"TelnetSession connecting to {self.server.get('alias', '?')} port {port}")
reader, writer = await telnetlib3.open_connection(
host=hostname,
port=port,
cols=self.cols,
rows=self.rows,
connect_minwait=0.5,
)
self._reader = reader
self._writer = writer
# Login sequence — wait for prompts and send credentials
await self._login_sequence(reader, writer, user, password)
# Main read loop
await self._read_loop(reader)
async def _login_sequence(self, reader, writer, user: str, password: str):
"""Wait for login/password prompts and send credentials."""
buf = ""
timeout = 10.0 # seconds to wait for login prompt
while self._running:
try:
data = await asyncio.wait_for(reader.read(4096), timeout=timeout)
except asyncio.TimeoutError:
log.debug("TelnetSession login sequence timed out waiting for prompt")
break
except Exception:
break
if not data:
break
if self.on_data:
self.on_data(data.encode("utf-8", errors="replace") if isinstance(data, str) else data)
buf += data if isinstance(data, str) else data.decode("utf-8", errors="replace")
buf_lower = buf.lower()
if "login:" in buf_lower or "username:" in buf_lower:
writer.write(user + "\r\n")
buf = ""
continue
if "password:" in buf_lower:
writer.write(password + "\r\n")
buf = ""
break # Login done, proceed to read loop
# If we see a shell prompt, login may not be required
if buf_lower.rstrip().endswith(("$", "#", ">")):
break
log.debug("TelnetSession login sequence complete")
async def _read_loop(self, reader):
"""Read data from telnet and forward to on_data callback."""
try:
while self._running:
try:
data = await asyncio.wait_for(reader.read(65536), timeout=0.5)
except asyncio.TimeoutError:
continue
except Exception:
break
if not data:
break
raw = data.encode("utf-8", errors="replace") if isinstance(data, str) else data
if self.on_data:
self.on_data(raw)
except Exception as e:
log.debug(f"TelnetSession read loop error: {e}")
def send(self, data: bytes):
"""Send data to the telnet session."""
if not self._running or self._writer is None or self._loop is None:
return
text = data.decode("utf-8", errors="replace")
try:
self._loop.call_soon_threadsafe(self._writer.write, text)
except RuntimeError:
self._running = False
if self.on_disconnect:
self.on_disconnect()
def resize(self, cols: int, rows: int):
"""Resize terminal — NAWS negotiation if supported, otherwise no-op."""
self.cols = cols
self.rows = rows
# telnetlib3 handles NAWS during initial negotiation;
# runtime resize requires protocol-level support which
# is not reliably available, so this is a best-effort no-op.
log.debug(f"TelnetSession resize requested: {cols}x{rows} (no-op)")
def disconnect(self):
"""Close telnet session and stop background thread."""
self._running = False
if self._writer is not None:
try:
self._writer.close()
except Exception as e:
log.debug(f"TelnetSession writer close: {e}")
self._writer = None
self._reader = None
if self._loop is not None:
try:
self._loop.call_soon_threadsafe(self._loop.stop)
except RuntimeError:
pass