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>
This commit is contained in:
180
core/telnet_client.py
Normal file
180
core/telnet_client.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user