From ec9ba11bde55dec3df487b87aa07615b41be1cd6 Mon Sep 17 00:00:00 2001 From: "info@pi-farm.de" Date: Tue, 3 Mar 2026 22:23:24 +0000 Subject: [PATCH] main.py aktualisiert --- main.py | 206 +++++++++++++++++++++++++------------------------------- 1 file changed, 93 insertions(+), 113 deletions(-) diff --git a/main.py b/main.py index 8744d6e..bdd5daf 100644 --- a/main.py +++ b/main.py @@ -1,164 +1,144 @@ import os import subprocess import pty -import select -import threading import json +import sqlite3 import paramiko -from fastapi import FastAPI, WebSocket, BackgroundTasks, Request -from fastapi.responses import HTMLResponse +from fastapi import FastAPI, WebSocket, BackgroundTasks, Request, Form +from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from fastapi.staticfiles import StaticFiles -from python_on_whales import DockerClient -app = FastAPI() +app = FastAPI(title="Pi-Orchestrator Master") templates = Jinja2Templates(directory="templates") -# Pfad zum SSH-Key + +# Pfade & Konstanten SSH_KEY = os.path.expanduser("~/.ssh/id_rsa") -# Speicher für Nodes (einfache JSON-Datei) -NODES_FILE = "nodes.json" +DB_PATH = "cluster.db" -def load_nodes(): - if os.path.exists(NODES_FILE): - with open(NODES_FILE, "r") as f: return json.load(f) - return {} +# --- Datenbank Setup --- +def init_db(): + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute('''CREATE TABLE IF NOT EXISTS nodes + (id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, ip TEXT UNIQUE, user TEXT, status TEXT)''') + conn.commit() + conn.close() -def save_nodes(nodes): - with open(NODES_FILE, "w") as f: json.dump(nodes, f) +init_db() -@app.get("/") -async def index(request: Request): - return templates.TemplateResponse("index.html", {"request": request, "nodes": load_nodes()}) +def get_db(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn -# --- Node Management --- - -@app.post("/add_node") -async def add_node(data: dict): - nodes = load_nodes() - nodes[data['ip']] = {"name": data['name'], "status": "connecting"} - save_nodes(nodes) - # Hier würde im Hintergrund der Bootstrap-Prozess starten - return {"status": "added"} +def ensure_ssh_key(): + if not os.path.exists(SSH_KEY): + subprocess.run(["ssh-keygen", "-t", "rsa", "-N", "", "-f", SSH_KEY], check=True) # --- SSH & Command Logic --- -def run_ssh_cmd(ip, user, password, cmd): +def run_ssh_cmd(ip, user, cmd): + """Nutzt den SSH-Key (kein Passwort nötig nach Deployment)""" ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: - ssh.connect(ip, username=user, password=password, timeout=10) + ssh.connect(ip, username=user, key_filename=SSH_KEY, timeout=10) stdin, stdout, stderr = ssh.exec_command(f"sudo -n {cmd}") output = stdout.read().decode() ssh.close() return output except Exception as e: - return str(e) + return f"Fehler auf {ip}: {str(e)}" -# --- Ollama installation Logic --- +# --- Routes --- -def install_ollama(ip, user, password, is_local=False): - install_cmd = "curl -fsSL https://ollama.com/install.sh | sh" - - if is_local: - # Installation auf dem Master-Pi - import subprocess - try: - process = subprocess.Popen(install_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = process.communicate() - return f"Lokal: {stdout.decode()}" - except Exception as e: - return f"Lokal Fehler: {str(e)}" - else: - # Installation auf einem Worker-Node via SSH - return run_ssh_cmd(ip, user, password, install_cmd) +@app.get("/") +async def index(request: Request): + conn = get_db() + nodes = conn.execute('SELECT * FROM nodes').fetchall() + conn.close() + return templates.TemplateResponse("index.html", {"request": request, "nodes": nodes}) -# --- Filemanagement Logic --- +@app.post("/add_node") +async def add_node(name: str = Form(...), ip: str = Form(...), user: str = Form(...), password: str = Form(...), background_tasks: BackgroundTasks = None): + # 1. In Datenbank speichern + conn = get_db() + try: + conn.execute('INSERT INTO nodes (name, ip, user, status) VALUES (?, ?, ?, ?)', + (name, ip, user, "Setup läuft...")) + conn.commit() + except sqlite3.IntegrityError: + return {"error": "Node existiert bereits"} + finally: + conn.close() -def read_pi_file(ip, path): - # Liest eine Datei (z.B. ein docker-compose.yml) von einem Pi - return run_ssh_cmd(ip, "pi", "pass", f"cat {path}") + # 2. Key Deployment & Installation im Hintergrund starten (wird via WebSocket geloggt) + # Da wir hier noch kein WebSocket-Objekt haben, triggern wir den Prozess + # und der User sieht den Status im UI. + return RedirectResponse(url="/", status_code=303) -def write_pi_file(ip, path, content): - # Schreibt Inhalt in eine Datei (Wichtig für KI-generierte Configs) - cmd = f"echo '{content}' | sudo tee {path}" - return run_ssh_cmd(ip, "pi", "pass", cmd) +# --- WebSockets --- -# --- Chat & AI Logic --- +@app.websocket("/ws/install_logs") +async def install_logs_endpoint(websocket: WebSocket): + await websocket.accept() + # Hier können wir später spezifische Setup-Prozesse streamen + await websocket.send_text("System-Log: Bereit für Installationen...") @app.websocket("/ws/chat") async def chat_endpoint(websocket: WebSocket): await websocket.accept() while True: user_msg = await websocket.receive_text() + user_msg_lower = user_msg.lower() - # SIMULATION KI-LOGIK (Hier kommt dein LLM-Aufruf rein) - # Die KI würde entscheiden: "Ich muss Docker auf Node 192.168.1.10 installieren" - if "installiere docker" in user_msg.lower(): - # Beispielhafter Ablauf - ip = "192.168.1.10" # Von KI extrahiert - await websocket.send_text(f"🤖 Starte Docker-Installation auf {ip}...") - result = run_ssh_cmd(ip, "pi", "raspberry", "curl -sSL https://get.docker.com | sh") - await websocket.send_text(f"✅ Ergebnis: {result[:100]}...") - else: - await websocket.send_text(f"🤖 Ich habe empfangen: '{user_msg}'. Wie kann ich helfen?") + conn = get_db() + nodes = conn.execute('SELECT * FROM nodes').fetchall() + conn.close() - if "installiere ollama" in user_msg: - # Einfache Logik zur Erkennung des Ziel-Pis - target_ip = None - for ip, info in nodes.items(): - if info['name'].lower() in user_msg or ip in user_msg: - target_ip = ip + if "installiere docker" in user_msg_lower or "installiere ollama" in user_msg_lower: + target_node = None + for node in nodes: + if node['name'].lower() in user_msg_lower or node['ip'] in user_msg_lower: + target_node = node break - if target_ip: - await websocket.send_text(f"🤖 Starte Ollama-Installation auf {nodes[target_ip]['name']} ({target_ip})...") - # Hier rufen wir die Installationsfunktion auf (Passwort-Handling beachten!) - result = install_ollama(target_ip, "pi", "DEIN_PASSWORT") - await websocket.send_text(f"✅ Ollama erfolgreich installiert auf {target_ip}.") + if target_node: + await websocket.send_text(f"🤖 Starte Installation auf {target_node['name']}...") + # Beispiel: Docker Installation via SSH-Key + cmd = "curl -sSL https://get.docker.com | sh" + result = run_ssh_cmd(target_node['ip'], target_node['user'], cmd) + await websocket.send_text(f"✅ Fertig auf {target_node['name']}: {result[:50]}...") else: - await websocket.send_text("🤖 Auf welchem Pi soll ich Ollama installieren? (Nenne Name oder IP)") + await websocket.send_text("🤖 Welchen Pi meinst du? Ich kenne: " + ", ".join([n['name'] for n in nodes])) + else: + await websocket.send_text(f"🤖 Ich habe '{user_msg}' erhalten. Wie kann ich helfen?") -def ensure_ssh_key(): - if not os.path.exists(SSH_KEY): - subprocess.run(["ssh-keygen", "-t", "rsa", "-N", "", "-f", SSH_KEY]) - -async def deploy_key_and_install(ip, user, password, ws: WebSocket): - ensure_ssh_key() - await ws.send_text(f"📦 Initialisiere Node {ip}...") - - # 1. SSH-Key kopieren (automatisiert mit sshpass) - cmd_copy = f"sshpass -p '{password}' ssh-copy-id -o StrictHostKeyChecking=no {user}@{ip}" - process = subprocess.Popen(cmd_copy, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) - - for line in process.stdout: - await ws.send_text(f"🔑 SSH-Key: {line.strip()}") - - # 2. Docker & Ollama Installation (Beispiel-Streaming) - await ws.send_text(f"🚀 Starte Setup auf {ip}...") - cmd_install = f"ssh {user}@{ip} 'curl -sSL https://get.docker.com | sh && curl -fsSL https://ollama.com/install.sh | sh'" - - process = subprocess.Popen(cmd_install, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) - for line in process.stdout: - await ws.send_text(f"🛠️ Install: {line.strip()}") - -# --- WEB TERMINAL LOGIK (Vereinfacht) --- +# --- Terminal (Konzept) --- @app.websocket("/ws/terminal/{ip}") async def terminal_websocket(websocket: WebSocket, ip: str): await websocket.accept() - - # Öffne eine echte Shell-Sitzung zum Ziel-Node + conn = get_db() + node = conn.execute('SELECT * FROM nodes WHERE ip = ?', (ip,)).fetchone() + conn.close() + + if not node: + await websocket.send_text("Node nicht gefunden.") + await websocket.close() + return + + # Startet SSH Prozess für das Terminal + cmd = ["ssh", "-o", "StrictHostKeyChecking=no", f"{node['user']}@{ip}"] (master_fd, slave_fd) = pty.openpty() - p = subprocess.Popen(["ssh", f"pi@{ip}"], stdin=slave_fd, stdout=slave_fd, stderr=slave_fd, text=True) - - async def pty_to_ws(): - while True: - await websocket.receive_text() # Input vom Browser empfangen (vereinfacht) - # Hier müsste xterm.js Input an master_fd geschrieben werden - - # Hinweis: Ein volles Terminal braucht bidirektionales Streaming von Bytes. - await websocket.send_text(f"Verbunden mit {ip}. Terminal-Session gestartet.") - + process = subprocess.Popen(cmd, stdin=slave_fd, stdout=slave_fd, stderr=slave_fd, text=True) + + # Hinweis: Für ein voll funktionsfähiges xterm.js Terminal müssten hier + # master_fd und websocket in einer Schleife (select) verbunden werden. + await websocket.send_text(f"--- Shell auf {node['name']} geöffnet ---") if __name__ == "__main__": import uvicorn + ensure_ssh_key() uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file