main.py aktualisiert

This commit is contained in:
2026-03-03 22:23:24 +00:00
parent ebe974125b
commit ec9ba11bde

200
main.py
View File

@@ -1,164 +1,144 @@
import os import os
import subprocess import subprocess
import pty import pty
import select
import threading
import json import json
import sqlite3
import paramiko import paramiko
from fastapi import FastAPI, WebSocket, BackgroundTasks, Request from fastapi import FastAPI, WebSocket, BackgroundTasks, Request, Form
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from python_on_whales import DockerClient
app = FastAPI() app = FastAPI(title="Pi-Orchestrator Master")
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
# Pfad zum SSH-Key
# Pfade & Konstanten
SSH_KEY = os.path.expanduser("~/.ssh/id_rsa") SSH_KEY = os.path.expanduser("~/.ssh/id_rsa")
# Speicher für Nodes (einfache JSON-Datei) DB_PATH = "cluster.db"
NODES_FILE = "nodes.json"
def load_nodes(): # --- Datenbank Setup ---
if os.path.exists(NODES_FILE): def init_db():
with open(NODES_FILE, "r") as f: return json.load(f) conn = sqlite3.connect(DB_PATH)
return {} 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): init_db()
with open(NODES_FILE, "w") as f: json.dump(nodes, f)
@app.get("/") def get_db():
async def index(request: Request): conn = sqlite3.connect(DB_PATH)
return templates.TemplateResponse("index.html", {"request": request, "nodes": load_nodes()}) conn.row_factory = sqlite3.Row
return conn
# --- Node Management --- def ensure_ssh_key():
if not os.path.exists(SSH_KEY):
@app.post("/add_node") subprocess.run(["ssh-keygen", "-t", "rsa", "-N", "", "-f", SSH_KEY], check=True)
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"}
# --- SSH & Command Logic --- # --- 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 = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try: 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}") stdin, stdout, stderr = ssh.exec_command(f"sudo -n {cmd}")
output = stdout.read().decode() output = stdout.read().decode()
ssh.close() ssh.close()
return output return output
except Exception as e: 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): @app.get("/")
install_cmd = "curl -fsSL https://ollama.com/install.sh | sh" 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})
if is_local: @app.post("/add_node")
# Installation auf dem Master-Pi async def add_node(name: str = Form(...), ip: str = Form(...), user: str = Form(...), password: str = Form(...), background_tasks: BackgroundTasks = None):
import subprocess # 1. In Datenbank speichern
conn = get_db()
try: try:
process = subprocess.Popen(install_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) conn.execute('INSERT INTO nodes (name, ip, user, status) VALUES (?, ?, ?, ?)',
stdout, stderr = process.communicate() (name, ip, user, "Setup läuft..."))
return f"Lokal: {stdout.decode()}" conn.commit()
except Exception as e: except sqlite3.IntegrityError:
return f"Lokal Fehler: {str(e)}" return {"error": "Node existiert bereits"}
else: finally:
# Installation auf einem Worker-Node via SSH conn.close()
return run_ssh_cmd(ip, user, password, install_cmd)
# --- Filemanagement Logic --- # 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 read_pi_file(ip, path): # --- WebSockets ---
# Liest eine Datei (z.B. ein docker-compose.yml) von einem Pi
return run_ssh_cmd(ip, "pi", "pass", f"cat {path}")
def write_pi_file(ip, path, content): @app.websocket("/ws/install_logs")
# Schreibt Inhalt in eine Datei (Wichtig für KI-generierte Configs) async def install_logs_endpoint(websocket: WebSocket):
cmd = f"echo '{content}' | sudo tee {path}" await websocket.accept()
return run_ssh_cmd(ip, "pi", "pass", cmd) # Hier können wir später spezifische Setup-Prozesse streamen
await websocket.send_text("System-Log: Bereit für Installationen...")
# --- Chat & AI Logic ---
@app.websocket("/ws/chat") @app.websocket("/ws/chat")
async def chat_endpoint(websocket: WebSocket): async def chat_endpoint(websocket: WebSocket):
await websocket.accept() await websocket.accept()
while True: while True:
user_msg = await websocket.receive_text() user_msg = await websocket.receive_text()
user_msg_lower = user_msg.lower()
# SIMULATION KI-LOGIK (Hier kommt dein LLM-Aufruf rein) conn = get_db()
# Die KI würde entscheiden: "Ich muss Docker auf Node 192.168.1.10 installieren" nodes = conn.execute('SELECT * FROM nodes').fetchall()
if "installiere docker" in user_msg.lower(): conn.close()
# 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?")
if "installiere ollama" in user_msg: if "installiere docker" in user_msg_lower or "installiere ollama" in user_msg_lower:
# Einfache Logik zur Erkennung des Ziel-Pis target_node = None
target_ip = None for node in nodes:
for ip, info in nodes.items(): if node['name'].lower() in user_msg_lower or node['ip'] in user_msg_lower:
if info['name'].lower() in user_msg or ip in user_msg: target_node = node
target_ip = ip
break break
if target_ip: if target_node:
await websocket.send_text(f"🤖 Starte Ollama-Installation auf {nodes[target_ip]['name']} ({target_ip})...") await websocket.send_text(f"🤖 Starte Installation auf {target_node['name']}...")
# Hier rufen wir die Installationsfunktion auf (Passwort-Handling beachten!) # Beispiel: Docker Installation via SSH-Key
result = install_ollama(target_ip, "pi", "DEIN_PASSWORT") cmd = "curl -sSL https://get.docker.com | sh"
await websocket.send_text(f"✅ Ollama erfolgreich installiert auf {target_ip}.") result = run_ssh_cmd(target_node['ip'], target_node['user'], cmd)
await websocket.send_text(f"✅ Fertig auf {target_node['name']}: {result[:50]}...")
else: 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(): # --- Terminal (Konzept) ---
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) ---
@app.websocket("/ws/terminal/{ip}") @app.websocket("/ws/terminal/{ip}")
async def terminal_websocket(websocket: WebSocket, ip: str): async def terminal_websocket(websocket: WebSocket, ip: str):
await websocket.accept() await websocket.accept()
conn = get_db()
node = conn.execute('SELECT * FROM nodes WHERE ip = ?', (ip,)).fetchone()
conn.close()
# Öffne eine echte Shell-Sitzung zum Ziel-Node 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() (master_fd, slave_fd) = pty.openpty()
p = subprocess.Popen(["ssh", f"pi@{ip}"], stdin=slave_fd, stdout=slave_fd, stderr=slave_fd, text=True) process = subprocess.Popen(cmd, 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.")
# 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__": if __name__ == "__main__":
import uvicorn import uvicorn
ensure_ssh_key()
uvicorn.run(app, host="0.0.0.0", port=8000) uvicorn.run(app, host="0.0.0.0", port=8000)