main.py aktualisiert
This commit is contained in:
206
main.py
206
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)
|
||||
Reference in New Issue
Block a user