diff --git a/main.py b/main.py index 26b5441..8d6ffa8 100644 --- a/main.py +++ b/main.py @@ -1,435 +1,353 @@ - - - - - Pi-Orchestrator AI Dashboard - +import os +import pty +import fcntl +import subprocess +import sqlite3 +import asyncio +import openai +import re +import httpx +from google import genai +from google.genai import types +import json +from fastapi import FastAPI, WebSocket, BackgroundTasks, Request, Form, WebSocketDisconnect +from fastapi.responses import RedirectResponse +from fastapi.templating import Jinja2Templates +from fastapi.staticfiles import StaticFiles +from dotenv import load_dotenv, set_key + +# Lade Umgebungsvariablen +load_dotenv() + +app = FastAPI() +static_path = os.path.join(os.path.dirname(__file__), "static") +app.mount("/static", StaticFiles(directory=static_path), name="static") +templates = Jinja2Templates(directory="templates") + +SSH_KEY = os.path.expanduser("~/.ssh/id_rsa") +DB_PATH = "cluster.db" +chat_history = [] +PROMPT_FILE = "system_prompt.txt" +ENV_FILE = os.path.join(os.path.dirname(__file__), ".env") + +# KI KONFIGURATION +AI_PROVIDER = os.getenv("AI_PROVIDER", "google").lower() +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "") +GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "") +OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://127.0.0.1:11434/v1") + +GOOGLE_MODEL = os.getenv("GOOGLE_MODEL", "gemini-2.0-flash") +OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o") +OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "llama3") + +# --- DATENBANK INITIALISIERUNG (ERWEITERT) --- +def init_db(): + conn = sqlite3.connect(DB_PATH) + # Spalten erweitert um sudo_password, os, arch, docker_installed + conn.execute(''' + CREATE TABLE IF NOT EXISTS nodes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + ip TEXT UNIQUE, + user TEXT, + sudo_password TEXT, + os TEXT DEFAULT 'Unbekannt', + arch TEXT DEFAULT 'Unbekannt', + docker_installed INTEGER DEFAULT 0, + status TEXT + ) + ''') + conn.commit() + conn.close() + +init_db() + +def get_db(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn + +def get_system_prompt(): + conn = get_db() + nodes = conn.execute('SELECT * FROM nodes').fetchall() + conn.close() - - - - - - - + node_info = "" + for n in nodes: + docker_str = "Ja" if n['docker_installed'] else "Nein" + node_info += f"- Name: {n['name']}, IP: {n['ip']}, User: {n['user']}, OS: {n['os']}, Arch: {n['arch']}, Docker: {docker_str}\n" - - - +# --- ERWEITERTES NODE BOOTSTRAPPING (Inventur) --- +async def bootstrap_node(ip, user, password): + await manager.broadcast(f"🔑 SSH-Handshake für {ip}...") + # 1. Key kopieren + ssh_copy_cmd = f"sshpass -p '{password}' ssh-copy-id -o StrictHostKeyChecking=no -i {SSH_KEY}.pub {user}@{ip}" + proc = subprocess.run(ssh_copy_cmd, shell=True, capture_output=True, text=True) -
-
🤖 KI-Orchestrator
+ if proc.returncode != 0: + await manager.broadcast(f"❌ Fehler beim Key-Copy für {ip}: {proc.stderr}") + return + + # 2. System-Infos abrufen (Inventur) + await manager.broadcast(f"🔍 Inventur auf {ip} wird durchgeführt...") + + # Befehlskette: Arch, OS-Name, Docker-Check + inspect_cmd = "uname -m && (lsb_release -ds || cat /etc/os-release | grep PRETTY_NAME | cut -d'=' -f2) && (command -v docker >/dev/null 2>&1 && echo '1' || echo '0')" + ssh_cmd = f"ssh -o StrictHostKeyChecking=no {user}@{ip} \"{inspect_cmd}\"" + + try: + output = subprocess.check_output(ssh_cmd, shell=True).decode().strip().split('\n') + arch = output[0] if len(output) > 0 else "Unbekannt" + # Mapping für Architektur + if "aarch64" in arch.lower(): arch = "arm64" + elif "x86_64" in arch.lower(): arch = "x86-64" -
- + os_name = output[1].replace('"', '') if len(output) > 1 else "Linux" + docker_val = int(output[2]) if len(output) > 2 else 0 + + status = "Docker Aktiv" if docker_val else "Bereit (Kein Docker)" + + # Datenbank Update + conn = get_db() + conn.execute(''' + UPDATE nodes SET os = ?, arch = ?, docker_installed = ?, status = ? + WHERE ip = ? + ''', (os_name, arch, docker_val, status, ip)) + conn.commit() + conn.close() + await manager.broadcast(f"✅ Node {ip} konfiguriert ({os_name}, {arch}).") + except Exception as e: + await manager.broadcast(f"⚠️ Inventur auf {ip} unvollständig: {e}") -
+# --- ROUTES --- - - - - - - -
- - -
- - - -
-
+@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}) -
-
-
- {% for node in nodes %} -
-
-
{{ node.name }}
-
- -
-
- -
- {{ node.ip }} - -
+@app.get("/api/node/{node_id}") +async def get_node(node_id: int): + conn = get_db() + node = conn.execute('SELECT * FROM nodes WHERE id = ?', (node_id,)).fetchone() + conn.close() + return dict(node) if node else {} -
- {{ node.os }} - {{ node.arch }} -
+@app.post("/add_node") +async def add_node(background_tasks: BackgroundTasks, name: str = Form(...), ip: str = Form(...), user: str = Form(...), password: str = Form(...)): + conn = get_db() + try: + # Speichere Initialdaten inkl. Sudo-Passwort + conn.execute(''' + INSERT INTO nodes (name, ip, user, sudo_password, status) + VALUES (?, ?, ?, ?, ?) + ''', (name, ip, user, password, "Kopplung...")) + conn.commit() + background_tasks.add_task(bootstrap_node, ip, user, password) + except sqlite3.IntegrityError: pass + finally: conn.close() + return RedirectResponse(url="/", status_code=303) -
- - {{ node.status }} -
- -
- {% endfor %} -
- -
+@app.post("/edit_node/{node_id}") +async def edit_node(node_id: int, name: str = Form(...), ip: str = Form(...), user: str = Form(...)): + conn = get_db() + conn.execute('UPDATE nodes SET name=?, ip=?, user=? WHERE id=?', (name, ip, user, node_id)) + conn.commit() + conn.close() + return RedirectResponse(url="/", status_code=303) -
-
-
-
-
💬 KI Chat
-
-
- - -
-
-
+@app.post("/remove_node/{node_id}") +async def remove_node(node_id: int): + conn = get_db() + conn.execute('DELETE FROM nodes WHERE id = ?', (node_id,)) + conn.commit() + conn.close() + return RedirectResponse(url="/", status_code=303) -
-
-
📜 System Logs
-
Warte auf Aufgaben...
-
-
+@app.get("/refresh_status/{node_id}") +async def refresh_status(node_id: int): + conn = get_db() + node = conn.execute('SELECT * FROM nodes WHERE id = ?', (node_id,)).fetchone() + if not node: return {"status": "Offline"} -
-
-
🖥️ Live Terminal
-
-
-
-
-
-
+ inspect_cmd = "uname -m && (lsb_release -ds || cat /etc/os-release | grep PRETTY_NAME | cut -d'=' -f2) && (command -v docker >/dev/null 2>&1 && echo '1' || echo '0')" + ssh_cmd = f"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=3 {node['user']}@{node['ip']} \"{inspect_cmd}\"" + + try: + output = subprocess.check_output(ssh_cmd, shell=True).decode().strip().split('\n') + arch = output[0]; os_name = output[1].replace('"', ''); docker_val = int(output[2]) + if "aarch64" in arch.lower(): arch = "arm64" + elif "x86_64" in arch.lower(): arch = "x86-64" + new_status = "Docker Aktiv" if docker_val else "Bereit (Kein Docker)" + + conn.execute('UPDATE nodes SET status=?, os=?, arch=?, docker_installed=? WHERE id=?', + (new_status, os_name, arch, docker_val, node_id)) + conn.commit() + result = {"status": new_status, "os": os_name, "arch": arch, "docker": docker_val} + except: + new_status = "Offline" + conn.execute('UPDATE nodes SET status=? WHERE id=?', (new_status, node_id)) + conn.commit() + result = {"status": new_status, "os": node['os'], "arch": node['arch'], "docker": node['docker_installed']} + + conn.close() + return result - +# --- WebSockets Terminal / Chat / Logs (Integration wie gehabt) --- +@app.websocket("/ws/install_logs") +async def log_websocket(websocket: WebSocket): + await manager.connect(websocket) + try: + while True: await websocket.receive_text() + except WebSocketDisconnect: manager.disconnect(websocket) - - - \ No newline at end of file +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file