Initial scaffold: LLM Trainer Dashboard
Full-stack app with FastAPI backend (SSH/paramiko, pipeline streaming, GPU stats, xterm.js terminal, Ollama model manager) and React + Tailwind frontend (8 panels: Connection, Documents, Pipeline, QA Pairs, Training, Terminal, Models, Config). Docker Compose included. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
177
backend/ssh_client.py
Normal file
177
backend/ssh_client.py
Normal file
@@ -0,0 +1,177 @@
|
||||
import base64
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import paramiko
|
||||
|
||||
|
||||
class SSHClient:
|
||||
def __init__(self):
|
||||
self.client: Optional[paramiko.SSHClient] = None
|
||||
self.connected = False
|
||||
self.host = ""
|
||||
self.username = ""
|
||||
self.port = 22
|
||||
self._keepalive_thread: Optional[threading.Thread] = None
|
||||
self._stop_keepalive = threading.Event()
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def connect(self, host: str, username: str, password: str = None,
|
||||
key_path: str = None, port: int = 22) -> bool:
|
||||
with self._lock:
|
||||
try:
|
||||
if self.client:
|
||||
self.client.close()
|
||||
|
||||
self.client = paramiko.SSHClient()
|
||||
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
kwargs = {"hostname": host, "port": port, "username": username, "timeout": 10}
|
||||
if key_path:
|
||||
kwargs["key_filename"] = key_path
|
||||
if password:
|
||||
kwargs["password"] = password
|
||||
|
||||
self.client.connect(**kwargs)
|
||||
self.connected = True
|
||||
self.host = host
|
||||
self.username = username
|
||||
self.port = port
|
||||
|
||||
self._stop_keepalive.clear()
|
||||
self._keepalive_thread = threading.Thread(target=self._keepalive_loop, daemon=True)
|
||||
self._keepalive_thread.start()
|
||||
return True
|
||||
except Exception as e:
|
||||
self.connected = False
|
||||
raise e
|
||||
|
||||
def disconnect(self):
|
||||
self._stop_keepalive.set()
|
||||
if self.client:
|
||||
self.client.close()
|
||||
self.connected = False
|
||||
|
||||
def _keepalive_loop(self):
|
||||
while not self._stop_keepalive.wait(30):
|
||||
try:
|
||||
transport = self.client.get_transport()
|
||||
if transport and transport.is_active():
|
||||
transport.send_ignore()
|
||||
else:
|
||||
self.connected = False
|
||||
break
|
||||
except Exception:
|
||||
self.connected = False
|
||||
break
|
||||
|
||||
def execute(self, command: str, use_conda: bool = True) -> tuple:
|
||||
if not self.is_connected():
|
||||
raise Exception("Not connected to SSH server")
|
||||
|
||||
if use_conda:
|
||||
full_cmd = (
|
||||
f"source /home/{self.username}/miniconda3/etc/profile.d/conda.sh && "
|
||||
f"conda activate synthetic-data && {command}"
|
||||
)
|
||||
else:
|
||||
full_cmd = command
|
||||
|
||||
_, stdout, stderr = self.client.exec_command(full_cmd)
|
||||
out = stdout.read().decode("utf-8", errors="replace")
|
||||
err = stderr.read().decode("utf-8", errors="replace")
|
||||
exit_code = stdout.channel.recv_exit_status()
|
||||
return out, err, exit_code
|
||||
|
||||
def execute_stream(self, command: str, use_conda: bool = True):
|
||||
"""Generator that yields output lines from a command."""
|
||||
if not self.is_connected():
|
||||
raise Exception("Not connected to SSH server")
|
||||
|
||||
if use_conda:
|
||||
full_cmd = (
|
||||
f"source /home/{self.username}/miniconda3/etc/profile.d/conda.sh && "
|
||||
f"conda activate synthetic-data && {command}"
|
||||
)
|
||||
else:
|
||||
full_cmd = command
|
||||
|
||||
transport = self.client.get_transport()
|
||||
channel = transport.open_session()
|
||||
channel.get_pty()
|
||||
channel.exec_command(full_cmd)
|
||||
|
||||
buffer = b""
|
||||
while True:
|
||||
if channel.recv_ready():
|
||||
data = channel.recv(4096)
|
||||
if not data:
|
||||
break
|
||||
buffer += data
|
||||
while b"\n" in buffer:
|
||||
line, buffer = buffer.split(b"\n", 1)
|
||||
yield line.decode("utf-8", errors="replace") + "\n"
|
||||
elif channel.exit_status_ready():
|
||||
if buffer:
|
||||
yield buffer.decode("utf-8", errors="replace")
|
||||
break
|
||||
else:
|
||||
time.sleep(0.05)
|
||||
|
||||
channel.close()
|
||||
|
||||
def open_shell_channel(self, term: str = "xterm-256color", width: int = 220, height: int = 50):
|
||||
"""Open an interactive shell channel for the terminal panel."""
|
||||
if not self.is_connected():
|
||||
raise Exception("Not connected to SSH server")
|
||||
|
||||
transport = self.client.get_transport()
|
||||
channel = transport.open_session()
|
||||
channel.get_pty(term=term, width=width, height=height)
|
||||
channel.invoke_shell()
|
||||
|
||||
# Auto-activate conda env
|
||||
activate = (
|
||||
f"source /home/{self.username}/miniconda3/etc/profile.d/conda.sh && "
|
||||
f"conda activate synthetic-data\n"
|
||||
)
|
||||
channel.send(activate)
|
||||
return channel
|
||||
|
||||
def upload_file(self, local_path: str, remote_path: str):
|
||||
if not self.is_connected():
|
||||
raise Exception("Not connected to SSH server")
|
||||
sftp = self.client.open_sftp()
|
||||
try:
|
||||
sftp.put(local_path, remote_path)
|
||||
finally:
|
||||
sftp.close()
|
||||
|
||||
def read_remote_file(self, remote_path: str) -> str:
|
||||
out, err, code = self.execute(f"cat '{remote_path}'", use_conda=False)
|
||||
if code != 0:
|
||||
raise Exception(f"Failed to read file: {err}")
|
||||
return out
|
||||
|
||||
def write_remote_file(self, remote_path: str, content: str):
|
||||
encoded = base64.b64encode(content.encode()).decode()
|
||||
cmd = f"echo '{encoded}' | base64 -d > '{remote_path}'"
|
||||
out, err, code = self.execute(cmd, use_conda=False)
|
||||
if code != 0:
|
||||
raise Exception(f"Failed to write file: {err}")
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
try:
|
||||
if self.client:
|
||||
transport = self.client.get_transport()
|
||||
if transport and transport.is_active():
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
self.connected = False
|
||||
return False
|
||||
|
||||
|
||||
# Singleton shared across all routes
|
||||
ssh_manager = SSHClient()
|
||||
Reference in New Issue
Block a user