From e3a80f7016cfc17d7f35024cafbcce8c4571b955 Mon Sep 17 00:00:00 2001 From: "info@pi-farm.de" Date: Fri, 6 Mar 2026 14:24:57 +0000 Subject: [PATCH 01/59] main.py aktualisiert --- main.py | 679 ++++++++++++++++++++++---------------------------------- 1 file changed, 268 insertions(+), 411 deletions(-) diff --git a/main.py b/main.py index d7fe784..3dc6712 100644 --- a/main.py +++ b/main.py @@ -4,491 +4,348 @@ import fcntl import subprocess import sqlite3 import asyncio -import openai +import time 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.responses import RedirectResponse, JSONResponse from fastapi.templating import Jinja2Templates from fastapi.staticfiles import StaticFiles -from dotenv import load_dotenv, set_key -# Lade Umgebungsvariablen aus der .env Datei -load_dotenv() +DB_PATH = "cluster.db" +SSH_KEY = os.path.expanduser("~/.ssh/id_rsa") 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") # NEU -# --- KI KONFIGURATION (Werte aus .env laden) --- -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") +# ----------------------------- +# DATABASE +# ----------------------------- -# Modelle aus .env laden (mit Standardwerten als Fallback) -GOOGLE_MODEL = os.getenv("GOOGLE_MODEL", "gemini-2.5-flash") -OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o") -OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "llama3") - -def get_system_prompt(): - # 1. Node Info aus DB holen - conn = get_db() - nodes = conn.execute('SELECT * FROM nodes').fetchall() - conn.close() - - node_info = "" - for n in nodes: - node_info += f"- Name: {n['name']}, IP: {n['ip']}, User: {n['user']}\n" - - # 2. Versuche den Prompt aus der Datei zu laden - if os.path.exists(PROMPT_FILE): - with open(PROMPT_FILE, "r", encoding="utf-8") as f: - template = f.read() - else: - # Fallback falls Datei fehlt - template = "Du bist ein Helfer. Nodes:\n{node_info}\nNutze cmd" - print(f"⚠️ Warnung: {PROMPT_FILE} nicht gefunden. Nutze Fallback.") - - return template.replace("{node_info}", node_info) - -# --- KI FUNKTIONEN --- - -async def get_ai_response(user_input, system_prompt): - global chat_history - - # 1. Die neue User-Nachricht dem Gedächtnis hinzufügen - chat_history.append({"role": "user", "content": user_input}) - - # 2. Gedächtnis auf die letzten 30 Nachrichten begrenzen - chat_history = chat_history[-30:] - - ai_msg = "" - - try: - if AI_PROVIDER == "openai" or AI_PROVIDER == "ollama": - messages = [{"role": "system", "content": system_prompt}] + chat_history - - # Sicherstellen, dass die URL für Ollama korrekt endet - if AI_PROVIDER == "ollama": - url = OLLAMA_BASE_URL - if not url.endswith('/v1') and not url.endswith('/v1/'): - url = url.rstrip('/') + '/v1' - key = "ollama" - model_to_use = OLLAMA_MODEL - else: - url = None # Benutzt Standard OpenAI URL - key = OPENAI_API_KEY - model_to_use = OPENAI_MODEL - - client = openai.OpenAI(base_url=url, api_key=key) - response = client.chat.completions.create( - model=model_to_use, - messages=messages - ) - ai_msg = response.choices[0].message.content - - elif AI_PROVIDER == "google": - # Für Google Gemini - if not GOOGLE_API_KEY: - return "Fehler: GOOGLE_API_KEY fehlt in der .env Datei!" - - client = genai.Client(api_key=GOOGLE_API_KEY) - - # Wir müssen unser Array in das spezielle Google-Format umwandeln - google_history = [] - - # Alle Nachrichten AUSSER der allerletzten (die aktuelle User-Frage) in die History packen - for msg in chat_history[:-1]: - role = "user" if msg["role"] == "user" else "model" - google_history.append( - types.Content(role=role, parts=[types.Part.from_text(text=msg["content"])]) - ) - - # Chat MIT dem übersetzten Gedächtnis starten - chat = client.chats.create( - model=GOOGLE_MODEL, - config=types.GenerateContentConfig(system_instruction=system_prompt), - history=google_history - ) - - # Jetzt erst die neue Nachricht an den Chat mit Gedächtnis schicken - response = chat.send_message(user_input) - ai_msg = response.text - - except Exception as e: - ai_msg = f"Fehler bei der KI-Anfrage: {e}" - print(f"KI Fehler: {e}") - - # 3. Die Antwort der KI ebenfalls ins Gedächtnis aufnehmen - chat_history.append({"role": "assistant", "content": ai_msg}) - - return ai_msg - -# --- DATENBANK INITIALISIERUNG --- def init_db(): conn = sqlite3.connect(DB_PATH) - conn.execute(''' - CREATE TABLE IF NOT EXISTS nodes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT, - ip TEXT UNIQUE, - user TEXT, - status TEXT - ) - ''') + + conn.execute(""" + CREATE TABLE IF NOT EXISTS nodes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + ip TEXT UNIQUE, + user TEXT, + + ssh_password TEXT, + sudo_password TEXT, + + os TEXT, + architecture TEXT, + + docker_installed INTEGER DEFAULT 0, + vnc_available INTEGER DEFAULT 0, + + last_seen INTEGER, + status TEXT + ) + """) + conn.commit() conn.close() init_db() + def get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn -# --- WebSocket Manager für Logs & Chat --- -class ConnectionManager: - def __init__(self): - self.active_connections: list[WebSocket] = [] - async def connect(self, websocket: WebSocket): - await websocket.accept() - self.active_connections.append(websocket) - def disconnect(self, websocket: WebSocket): - if websocket in self.active_connections: - self.active_connections.remove(websocket) - async def broadcast(self, message: str): - for connection in self.active_connections: +# ----------------------------- +# NODE DISCOVERY +# ----------------------------- + +async def detect_node_info(ip, user): + + cmds = { + "os": "grep '^ID=' /etc/os-release 2>/dev/null | cut -d= -f2", + "arch": "uname -m", + "docker": "command -v docker >/dev/null 2>&1 && echo 1 || echo 0", + "vnc": "pgrep -f vnc >/dev/null 2>&1 && echo 1 || echo 0" + } + + results = {} + + for key, cmd in cmds.items(): + ssh_cmd = f"ssh -o ConnectTimeout=3 -o StrictHostKeyChecking=no {user}@{ip} '{cmd}'" + + try: + out = subprocess.check_output(ssh_cmd, shell=True).decode().strip() + except: + out = "" + + results[key] = out + + return results + +# ----------------------------- +# NODE MONITOR +# ----------------------------- + +async def node_monitor_loop(): + + while True: + + conn = get_db() + nodes = conn.execute("SELECT * FROM nodes").fetchall() + + for node in nodes: + + ip = node["ip"] + user = node["user"] + try: - await connection.send_text(message) + + ssh_cmd = f"ssh -o ConnectTimeout=2 -o StrictHostKeyChecking=no {user}@{ip} 'echo online'" + subprocess.check_output(ssh_cmd, shell=True) + + conn.execute( + "UPDATE nodes SET status=?, last_seen=? WHERE id=?", + ("Online", int(time.time()), node["id"]) + ) + except: - pass -manager = ConnectionManager() + conn.execute( + "UPDATE nodes SET status=? WHERE id=?", + ("Offline", node["id"]) + ) + + conn.commit() + conn.close() + + await asyncio.sleep(60) + + +@app.on_event("startup") +async def start_monitor(): + asyncio.create_task(node_monitor_loop()) + +# ----------------------------- +# SSH BOOTSTRAP +# ----------------------------- + +async def bootstrap_node(ip, user, password): -# --- SSH Handshake (Nur Key kopieren) --- -async def bootstrap_ssh_only(ip, user, password): - await manager.broadcast(f"🔑 Initialisiere SSH-Handshake für {ip}...") - # Nutzt sshpass um den Key einmalig mit Passwort zu hinterlegen ssh_copy_cmd = f"sshpass -p '{password}' ssh-copy-id -o StrictHostKeyChecking=no -i {SSH_KEY}.pub {user}@{ip}" - - process = subprocess.Popen(ssh_copy_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) - for line in process.stdout: - await manager.broadcast(f"SSH: {line.strip()}") - + + subprocess.call(ssh_copy_cmd, shell=True) + + info = await detect_node_info(ip, user) + conn = get_db() - conn.execute('UPDATE nodes SET status = "Bereit (Kein Docker)" WHERE ip = ?', (ip,)) + + conn.execute( + """ + UPDATE nodes + SET os=?, architecture=?, docker_installed=?, vnc_available=?, status=? + WHERE ip=? + """, + ( + info["os"], + info["arch"], + int(info["docker"] or 0), + int(info["vnc"] or 0), + "Online", + ip + ) + ) + conn.commit() conn.close() - await manager.broadcast(f"✅ Node {ip} ist verbunden. KI kann nun Befehle senden.") -# --- Routen --- +# ----------------------------- +# ROUTES +# ----------------------------- @app.get("/") async def index(request: Request): + conn = get_db() - nodes = conn.execute('SELECT * FROM nodes').fetchall() + nodes = conn.execute("SELECT * FROM nodes").fetchall() conn.close() + return templates.TemplateResponse("index.html", {"request": request, "nodes": nodes}) + @app.post("/add_node") -async def add_node(background_tasks: BackgroundTasks, name: str = Form(...), ip: str = Form(...), user: str = Form(...), password: str = Form(...)): +async def add_node( + background_tasks: BackgroundTasks, + name: str = Form(...), + ip: str = Form(...), + user: str = Form(...), + password: str = Form(...) +): + conn = get_db() - try: - conn.execute('INSERT INTO nodes (name, ip, user, status) VALUES (?, ?, ?, ?)', (name, ip, user, "Kopplung...")) - conn.commit() - background_tasks.add_task(bootstrap_ssh_only, ip, user, password) - except sqlite3.IntegrityError: pass - finally: conn.close() - return RedirectResponse(url="/", status_code=303) + + conn.execute( + """ + INSERT INTO nodes (name, ip, user, ssh_password, status) + VALUES (?, ?, ?, ?, ?) + """, + (name, ip, user, password, "Connecting") + ) + + conn.commit() + conn.close() + + background_tasks.add_task(bootstrap_node, ip, user, password) + + return RedirectResponse("/", 303) + @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.execute("DELETE FROM nodes WHERE id=?", (node_id,)) conn.commit() conn.close() - return RedirectResponse(url="/", status_code=303) -@app.get("/api/settings") -async def get_settings(): - return { - "provider": AI_PROVIDER, - "google_model": GOOGLE_MODEL, - "openai_model": OPENAI_MODEL, - "ollama_model": OLLAMA_MODEL, - "ollama_base_url": OLLAMA_BASE_URL # URL ans Frontend schicken - } + return RedirectResponse("/", 303) -@app.post("/api/settings") -async def update_settings(request: Request): - # WICHTIG: OLLAMA_BASE_URL als global deklarieren - global AI_PROVIDER, GOOGLE_MODEL, OPENAI_MODEL, OLLAMA_MODEL, OLLAMA_BASE_URL - - data = await request.json() - provider = data.get("provider") - model = data.get("model") - ollama_url = data.get("ollama_base_url") # URL vom Frontend empfangen - - if provider: - AI_PROVIDER = provider - set_key(ENV_FILE, "AI_PROVIDER", provider) - - if provider == "google" and model: - GOOGLE_MODEL = model - set_key(ENV_FILE, "GOOGLE_MODEL", model) - elif provider == "openai" and model: - OPENAI_MODEL = model - set_key(ENV_FILE, "OPENAI_MODEL", model) - elif provider == "ollama" and model: - OLLAMA_MODEL = model - set_key(ENV_FILE, "OLLAMA_MODEL", model) - # Wenn eine Ollama-URL mitgeschickt wurde, speichern wir sie - if ollama_url: - OLLAMA_BASE_URL = ollama_url - set_key(ENV_FILE, "OLLAMA_BASE_URL", ollama_url) +@app.get("/refresh_status/{node_id}") +async def refresh_status(node_id: int): - return {"status": "success"} - -@app.get("/api/models") -async def get_models(provider: str, url: str = None): - try: - models = [] - - if provider == "ollama" and url: - # Das Backend hat keine CORS-Probleme und fragt Ollama direkt - clean_url = url.replace("/v1", "").rstrip("/") - async with httpx.AsyncClient() as client: - response = await client.get(f"{clean_url}/api/tags", timeout=5.0) - data = response.json() - models = [m["name"] for m in data.get("models", [])] - - elif provider == "openai": - if not OPENAI_API_KEY or "hier" in OPENAI_API_KEY: return {"models": []} - # Hier greift das Backend auf deinen OpenAI API-Key zu - import openai - client = openai.AsyncOpenAI(api_key=OPENAI_API_KEY) - response = await client.models.list() - # Nur GPT-Modelle filtern, um die Liste sauber zu halten - models = [m.id for m in response.data if "gpt" in m.id or "o1" in m.id] - models.sort() - - elif provider == "google": - if not GOOGLE_API_KEY: - return {"models": ["API-Key fehlt"]} - - client = genai.Client(api_key=GOOGLE_API_KEY) - models = [] - - # Im neuen SDK (google-genai) heißt das Feld 'supported_actions' - for m in client.models.list(): - if 'generateContent' in m.supported_actions: - # Wir nehmen den Namen und entfernen das 'models/' Präfix - model_name = m.name.replace("models/", "") - models.append(model_name) - - models.sort() - - return {"models": models} - - except Exception as e: - print(f"Fehler beim Abrufen der Modelle für {provider}: {str(e)}") - return {"models": []} # Gibt eine leere Liste zurück -> Frontend nutzt Fallback - -# --- WebSockets --- - -@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) - -@app.websocket("/ws/terminal/{ip}") -async def terminal_websocket(websocket: WebSocket, ip: str): - await websocket.accept() conn = get_db() - node = conn.execute('SELECT * FROM nodes WHERE ip = ?', (ip,)).fetchone() + node = conn.execute("SELECT * FROM nodes WHERE id=?", (node_id,)).fetchone() + + if not node: + return JSONResponse({"status": "Unknown"}) + + ip = node["ip"] + user = node["user"] + + try: + + ssh_cmd = f"ssh -o ConnectTimeout=2 -o StrictHostKeyChecking=no {user}@{ip} 'echo online'" + subprocess.check_output(ssh_cmd, shell=True) + + status = "Online" + + except: + + status = "Offline" + + conn.execute("UPDATE nodes SET status=? WHERE id=?", (status, node_id)) + conn.commit() + conn.close() + + return {"status": status} + +# ----------------------------- +# NODE EDIT +# ----------------------------- + +@app.get("/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() + + if not node: + return JSONResponse({}) + + return dict(node) + + +@app.post("/update_node/{node_id}") +async def update_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("/", 303) + +# ----------------------------- +# TERMINAL +# ----------------------------- + +@app.websocket("/ws/terminal/{ip}") +async def terminal(websocket: WebSocket, ip: str): + + await websocket.accept() + + conn = get_db() + node = conn.execute("SELECT * FROM nodes WHERE ip=?", (ip,)).fetchone() conn.close() if not node: - await websocket.send_text("\r\nFehler: Node nicht gefunden.\r\n") await websocket.close() return - # Pseudo-Terminal für interaktive SSH-Session master_fd, slave_fd = pty.openpty() + proc = await asyncio.create_subprocess_exec( - "ssh", "-o", "StrictHostKeyChecking=no", "-t", f"{node['user']}@{ip}", - stdin=slave_fd, stdout=slave_fd, stderr=slave_fd + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-t", + f"{node['user']}@{ip}", + stdin=slave_fd, + stdout=slave_fd, + stderr=slave_fd ) async def pty_to_ws(): - # Setzt den Master-FD auf non-blocking + fl = fcntl.fcntl(master_fd, fcntl.F_GETFL) fcntl.fcntl(master_fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) - try: - while True: - await asyncio.sleep(0.01) - try: - data = os.read(master_fd, 1024).decode(errors='ignore') - if data: - await websocket.send_text(data) - except BlockingIOError: - continue - except Exception: pass + + while True: + + await asyncio.sleep(0.01) + + try: + data = os.read(master_fd, 1024).decode(errors="ignore") + + if data: + await websocket.send_text(data) + + except: + pass async def ws_to_pty(): + try: while True: + data = await websocket.receive_text() os.write(master_fd, data.encode()) - except Exception: pass - try: - await asyncio.gather(pty_to_ws(), ws_to_pty()) - finally: - if proc.returncode is None: proc.terminate() - os.close(master_fd) - os.close(slave_fd) + except: + pass -# --- WEBSOCKET CHAT UPDATE --- - -@app.websocket("/ws/chat") -async def chat_endpoint(websocket: WebSocket): - await websocket.accept() - - # Check ob Key vorhanden ist - if AI_PROVIDER == "google" and not GOOGLE_API_KEY: - await websocket.send_text("⚠️ **Konfigurationsfehler:** Kein GOOGLE_API_KEY in der `.env` gefunden!") - elif AI_PROVIDER == "openai" and not OPENAI_API_KEY: - await websocket.send_text("⚠️ **Konfigurationsfehler:** Kein OPENAI_API_KEY in der `.env` gefunden!") - - try: - while True: - user_msg = await websocket.receive_text() - sys_prompt = get_system_prompt() - ai_response = await get_ai_response(user_msg, sys_prompt) - - # Befehle extrahieren - commands_to_run = re.findall(r'(.*?)', ai_response, re.IGNORECASE | re.DOTALL) - clean_chat_msg = re.sub(r'.*?', '', ai_response, flags=re.IGNORECASE | re.DOTALL).strip() - - if clean_chat_msg: - await websocket.send_text(clean_chat_msg) - - if commands_to_run: - # Liste für alle laufenden Tasks erstellen - tasks = [] - for target_ip, cmd in commands_to_run: - conn = get_db() - node = conn.execute('SELECT * FROM nodes WHERE ip = ? OR name = ?', (target_ip.strip(), target_ip.strip())).fetchone() - conn.close() - - if node: - # Wir erstellen den Task, starten ihn aber noch nicht separat - tasks.append(run_remote_task(node['ip'], node['user'], cmd.strip(), "KI-Kommando")) - - if tasks: - # Dem Nutzer im Chat kurz Bescheid geben - await websocket.send_text("ℹ️ *Warte auf Rückmeldungen der Nodes...*") - - # Jetzt werden alle SSH-Befehle gleichzeitig gestartet und abgewartet - await asyncio.gather(*tasks) - - # Sobald asyncio.gather fertig ist, geht es hier weiter mit dem Follow-up: - follow_up_prompt = "Die Befehle wurden ausgeführt. Bitte fasse die Ergebnisse kurz zusammen." - ai_summary = await get_ai_response(follow_up_prompt, sys_prompt) - - await websocket.send_text("--- Ergebnis-Zusammenfassung ---") - await websocket.send_text(ai_summary) - - except Exception as e: - print(f"Chat Fehler: {e}") - -async def run_remote_task(ip, user, cmd, task_name): - global chat_history # Zugriff auf das Gedächtnis der KI - - await manager.broadcast(f"🚀 KI-Task gestartet: {cmd} auf {ip}") - - ssh_cmd = f"ssh -o StrictHostKeyChecking=no {user}@{ip} '{cmd}'" - - process = await asyncio.create_subprocess_shell( - ssh_cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.STDOUT - ) - - # Hier speichern wir die komplette Terminal-Ausgabe - full_output = "" - - while True: - line = await process.stdout.readline() - if not line: - break - decoded_line = line.decode('utf-8', errors='ignore').strip() - if decoded_line: - await manager.broadcast(f"🛠️ {decoded_line}") - full_output += decoded_line + "\n" - # Gib dem Event-Loop kurz Zeit, andere Tasks (wie WebSockets) zu bedienen - await asyncio.sleep(0.001) - - await process.wait() - - # --- NEU: Feedback an die KI --- - # Wir bereiten den Bericht vor - if not full_output.strip(): - full_output = "Befehl wurde ohne Ausgabe ausgeführt (Exit Code 0)." - - system_report = f"[SYSTEM-RÜCKMELDUNG] Der Befehl '{cmd}' auf Node {ip} wurde beendet. Ausgabe des Terminals:\n{full_output}" - - # Wir schmuggeln den Bericht als "User"-Nachricht in den Verlauf, - # damit die KI beim nächsten Mal weiß, was passiert ist. - chat_history.append({"role": "user", "content": system_report}) - # ------------------------------- - - if "docker" in cmd.lower() and "install" in cmd.lower(): - # Kleiner Bonus: Nur updaten, wenn wirklich installiert wird - await manager.broadcast(f"✨ Bitte aktualisiere den Status für {ip} manuell über das Refresh-Icon.") - - await manager.broadcast(f"✅ Befehl auf {ip} abgeschlossen.") - -# --- Neuer Endpunkt: Manueller Refresh-Check --- -@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 node: - # Kurzer Check via SSH, ob Docker antwortet - check_cmd = "command -v docker >/dev/null 2>&1 && echo 'Docker Aktiv' || echo 'Bereit (Kein Docker)'" - ssh_cmd = f"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=2 {node['user']}@{node['ip']} \"{check_cmd}\"" - try: - # Wir führen den Befehl aus - new_status = subprocess.check_output(ssh_cmd, shell=True).decode().strip() - except Exception: - new_status = "Offline/Fehler" - - conn.execute('UPDATE nodes SET status = ? WHERE id = ?', (new_status, node_id)) - conn.commit() - conn.close() - return {"status": new_status} # Wir senden nur den Status zurück - - conn.close() - return {"status": "Unbekannt"} + await asyncio.gather(pty_to_ws(), ws_to_pty()) if __name__ == "__main__": + import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file + + uvicorn.run(app, host="0.0.0.0", port=8000) From 7a5845a8a0af6236bd7d37555e44d1cbc8703eb2 Mon Sep 17 00:00:00 2001 From: "info@pi-farm.de" Date: Fri, 6 Mar 2026 14:25:32 +0000 Subject: [PATCH 02/59] templates/index.html aktualisiert --- templates/index.html | 570 ++++++++++++------------------------------- 1 file changed, 156 insertions(+), 414 deletions(-) diff --git a/templates/index.html b/templates/index.html index 3680a74..0663394 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,445 +1,187 @@ - - Pi-Orchestrator AI Dashboard - - - - + +Node Dashboard + - - - - - - + - - -
-
🤖 KI-Orchestrator
- -
- -
+ - - - - - - -
- - -
- - - -
-
+
+

🖥 Node Cluster

+ +
-
-
- -
- {% for node in nodes %} -
-
-
{{ node.name }}
-
- -
-
-
- {{ node.ip }} - -
-
- - {{ node.status }} -
- -
- {% endfor %} -
- -
+
+{% for node in nodes %} -
-
-
-
-
💬 KI Chat
-
-
- - -
-
-
+
-
-
-
📜 System Logs
-
Warte auf Aufgaben...
-
-
+
+
{{node.name}}
-
-
-
🖥️ Live Terminal
-
-
-
-
-
-
+{% if node.status == 'Online' %} +
+{% else %} +
+{% endif %} +
- +
+{{node.ip}} +
- - loadSettings(); - }); - - \ No newline at end of file + From 127c3365d0955b073055dddbfb7afe2dc0f328a1 Mon Sep 17 00:00:00 2001 From: "info@pi-farm.de" Date: Fri, 6 Mar 2026 14:31:10 +0000 Subject: [PATCH 03/59] README.md aktualisiert --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7120df9..148643e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # PiDoBot Setup: -curl -sSL https://git.pi-farm.de/pi-farm/PiDoBot/raw/branch/main/setup.sh | bash +curl -sSL https://git.pi-farm.de/pi-farm/PiDoBot/raw/branch/dev/setup.sh | bash From 3d10127cd780ed75a5cb84fea4d7b0c61cf84dea Mon Sep 17 00:00:00 2001 From: "info@pi-farm.de" Date: Fri, 6 Mar 2026 14:33:14 +0000 Subject: [PATCH 04/59] setup.sh aktualisiert --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index b6f561a..990bbd7 100644 --- a/setup.sh +++ b/setup.sh @@ -25,7 +25,7 @@ sudo apt-get install -y wget sshpass python3-pip python3-venv # 3. Repository klonen if [ ! -d "$INSTALL_DIR" ]; then echo "--- Klone Repository von $REPO_URL..." - git clone "$REPO_URL" "$INSTALL_DIR" + git clone --branch dev --single-branch "$REPO_URL" "$INSTALL_DIR" else echo "--- Verzeichnis $INSTALL_DIR existiert bereits. Überspringe Klonen..." fi From 745e50508e1499c3982cdc4c4fe8a658d74565be Mon Sep 17 00:00:00 2001 From: "info@pi-farm.de" Date: Fri, 6 Mar 2026 14:51:40 +0000 Subject: [PATCH 05/59] templates/index.html aktualisiert --- templates/index.html | 480 +++++++++++++++++++++++++++++++------------ 1 file changed, 349 insertions(+), 131 deletions(-) diff --git a/templates/index.html b/templates/index.html index 0663394..bcb7476 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2,185 +2,403 @@ -Node Dashboard - + +PiDoBot Dashboard + + + + - +
-
-

🖥 Node Cluster

- -
+ + -
-{% for node in nodes %} + + -
+ + -
-
{{node.name}}
+ -{% if node.status == 'Online' %} -
-{% else %} -
-{% endif %} -
+ -
-{{node.ip}} -
+ -
+
-{{node.os}} -{{node.architecture}} - -{% if node.docker_installed %} -🐳 Docker -{% endif %} - -🔐 SSH - -{% if node.vnc_available %} -🖥 VNC -{% endif %} - -
- -
- - - - - -
- -
- -
- -
- -{% endfor %} - - - - - - - - - - - +
+ + + From 9b9bc12731163ba48c9db34e705256cf549ac1bc Mon Sep 17 00:00:00 2001 From: "info@pi-farm.de" Date: Fri, 6 Mar 2026 14:52:56 +0000 Subject: [PATCH 06/59] main.py aktualisiert --- main.py | 526 +++++++++++++++++++++++++++----------------------------- 1 file changed, 251 insertions(+), 275 deletions(-) diff --git a/main.py b/main.py index 3dc6712..15846fc 100644 --- a/main.py +++ b/main.py @@ -4,24 +4,27 @@ import fcntl import subprocess import sqlite3 import asyncio -import time import re -from fastapi import FastAPI, WebSocket, BackgroundTasks, Request, Form, WebSocketDisconnect -from fastapi.responses import RedirectResponse, JSONResponse -from fastapi.templating import Jinja2Templates +import json +import httpx +from dotenv import load_dotenv, set_key +from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel + +load_dotenv() + +app = FastAPI() + +STATIC_DIR = os.path.join(os.path.dirname(__file__),"static") +app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") DB_PATH = "cluster.db" SSH_KEY = os.path.expanduser("~/.ssh/id_rsa") -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") - -# ----------------------------- +# ------------------------------------------------- # DATABASE -# ----------------------------- +# ------------------------------------------------- def init_db(): conn = sqlite3.connect(DB_PATH) @@ -32,18 +35,14 @@ def init_db(): name TEXT, ip TEXT UNIQUE, user TEXT, - - ssh_password TEXT, sudo_password TEXT, - os TEXT, - architecture TEXT, - - docker_installed INTEGER DEFAULT 0, - vnc_available INTEGER DEFAULT 0, - - last_seen INTEGER, - status TEXT + arch TEXT, + docker INTEGER DEFAULT 0, + ssh INTEGER DEFAULT 1, + vnc INTEGER DEFAULT 0, + status TEXT DEFAULT 'unknown', + last_seen TEXT ) """) @@ -58,294 +57,271 @@ def get_db(): conn.row_factory = sqlite3.Row return conn -# ----------------------------- -# NODE DISCOVERY -# ----------------------------- +# ------------------------------------------------- +# MODELS +# ------------------------------------------------- -async def detect_node_info(ip, user): +class NodeCreate(BaseModel): + name:str + ip:str + user:str - cmds = { - "os": "grep '^ID=' /etc/os-release 2>/dev/null | cut -d= -f2", - "arch": "uname -m", - "docker": "command -v docker >/dev/null 2>&1 && echo 1 || echo 0", - "vnc": "pgrep -f vnc >/dev/null 2>&1 && echo 1 || echo 0" - } +class NodeUpdate(BaseModel): + name:str + ip:str + user:str - results = {} +class AISettings(BaseModel): + provider:str + model:str + ollama:str - for key, cmd in cmds.items(): - ssh_cmd = f"ssh -o ConnectTimeout=3 -o StrictHostKeyChecking=no {user}@{ip} '{cmd}'" +# ------------------------------------------------- +# NODE API +# ------------------------------------------------- - try: - out = subprocess.check_output(ssh_cmd, shell=True).decode().strip() - except: - out = "" +@app.get("/nodes") +def get_nodes(): - results[key] = out + conn = get_db() - return results + rows = conn.execute("SELECT * FROM nodes").fetchall() -# ----------------------------- -# NODE MONITOR -# ----------------------------- + conn.close() -async def node_monitor_loop(): + return [dict(r) for r in rows] + + +@app.post("/nodes") +def add_node(node:NodeCreate): + + conn = get_db() + + conn.execute("INSERT INTO nodes(name,ip,user) VALUES(?,?,?)", + (node.name,node.ip,node.user)) + + conn.commit() + + conn.close() + + return {"status":"ok"} + + +@app.put("/nodes/{node_id}") +def update_node(node_id:int,node:NodeUpdate): + + conn = get_db() + + conn.execute(""" + UPDATE nodes + SET name=?, ip=?, user=? + WHERE id=? + """, + (node.name,node.ip,node.user,node_id)) + + conn.commit() + + conn.close() + + return {"status":"updated"} + +# ------------------------------------------------- +# NODE SCANNING +# ------------------------------------------------- + +async def run_ssh(ip,user,cmd): + + ssh_cmd=[ + "ssh", + "-o","StrictHostKeyChecking=no", + f"{user}@{ip}", + cmd + ] + + proc = await asyncio.create_subprocess_exec( + *ssh_cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + + out,_ = await proc.communicate() + + return out.decode().strip() + + +async def detect_node(node): + + ip=node['ip'] + user=node['user'] + + try: + + arch = await run_ssh(ip,user,"uname -m") + + os_release = await run_ssh(ip,user,"cat /etc/os-release || uname") + + docker = await run_ssh(ip,user,"docker --version || echo nodocker") + + vnc = await run_ssh(ip,user,"pgrep Xtightvnc || echo novnc") + + os_name="unknown" + + if "debian" in os_release.lower(): + os_name="debian" + + elif "fedora" in os_release.lower(): + os_name="fedora" + + elif "raspbian" in os_release.lower(): + os_name="raspberrypi" + + elif "ubuntu" in os_release.lower(): + os_name="ubuntu" + + docker_installed = 0 if "nodocker" in docker else 1 + + vnc_enabled = 0 if "novnc" in vnc else 1 + + status="online" + + except: + + arch="" + os_name="" + docker_installed=0 + vnc_enabled=0 + status="offline" + + conn=get_db() + + conn.execute(""" + UPDATE nodes + SET os=?,arch=?,docker=?,vnc=?,status=?,last_seen=datetime('now') + WHERE id=? + """, + (os_name,arch,docker_installed,vnc_enabled,status,node['id'])) + + conn.commit() + + conn.close() + + +async def scan_nodes(): while True: - conn = get_db() - nodes = conn.execute("SELECT * FROM nodes").fetchall() + conn=get_db() - for node in nodes: + nodes=conn.execute("SELECT * FROM nodes").fetchall() - ip = node["ip"] - user = node["user"] - - try: - - ssh_cmd = f"ssh -o ConnectTimeout=2 -o StrictHostKeyChecking=no {user}@{ip} 'echo online'" - subprocess.check_output(ssh_cmd, shell=True) - - conn.execute( - "UPDATE nodes SET status=?, last_seen=? WHERE id=?", - ("Online", int(time.time()), node["id"]) - ) - - except: - - conn.execute( - "UPDATE nodes SET status=? WHERE id=?", - ("Offline", node["id"]) - ) - - conn.commit() conn.close() + tasks=[] + + for n in nodes: + tasks.append(detect_node(n)) + + if tasks: + await asyncio.gather(*tasks) + await asyncio.sleep(60) @app.on_event("startup") -async def start_monitor(): - asyncio.create_task(node_monitor_loop()) +async def start_scanner(): -# ----------------------------- -# SSH BOOTSTRAP -# ----------------------------- + asyncio.create_task(scan_nodes()) -async def bootstrap_node(ip, user, password): +# ------------------------------------------------- +# TERMINAL WEBSOCKET +# ------------------------------------------------- - ssh_copy_cmd = f"sshpass -p '{password}' ssh-copy-id -o StrictHostKeyChecking=no -i {SSH_KEY}.pub {user}@{ip}" - - subprocess.call(ssh_copy_cmd, shell=True) - - info = await detect_node_info(ip, user) - - conn = get_db() - - conn.execute( - """ - UPDATE nodes - SET os=?, architecture=?, docker_installed=?, vnc_available=?, status=? - WHERE ip=? - """, - ( - info["os"], - info["arch"], - int(info["docker"] or 0), - int(info["vnc"] or 0), - "Online", - ip - ) - ) - - conn.commit() - conn.close() - -# ----------------------------- -# 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}) - - -@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() - - conn.execute( - """ - INSERT INTO nodes (name, ip, user, ssh_password, status) - VALUES (?, ?, ?, ?, ?) - """, - (name, ip, user, password, "Connecting") - ) - - conn.commit() - conn.close() - - background_tasks.add_task(bootstrap_node, ip, user, password) - - return RedirectResponse("/", 303) - - -@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("/", 303) - - -@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 JSONResponse({"status": "Unknown"}) - - ip = node["ip"] - user = node["user"] - - try: - - ssh_cmd = f"ssh -o ConnectTimeout=2 -o StrictHostKeyChecking=no {user}@{ip} 'echo online'" - subprocess.check_output(ssh_cmd, shell=True) - - status = "Online" - - except: - - status = "Offline" - - conn.execute("UPDATE nodes SET status=? WHERE id=?", (status, node_id)) - conn.commit() - conn.close() - - return {"status": status} - -# ----------------------------- -# NODE EDIT -# ----------------------------- - -@app.get("/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() - - if not node: - return JSONResponse({}) - - return dict(node) - - -@app.post("/update_node/{node_id}") -async def update_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("/", 303) - -# ----------------------------- -# TERMINAL -# ----------------------------- - -@app.websocket("/ws/terminal/{ip}") -async def terminal(websocket: WebSocket, ip: str): +@app.websocket("/ws/terminal") +async def terminal_ws(websocket:WebSocket): await websocket.accept() - conn = get_db() - node = conn.execute("SELECT * FROM nodes WHERE ip=?", (ip,)).fetchone() - conn.close() + pid,fd = pty.fork() - if not node: - await websocket.close() - return + if pid==0: + os.execvp("bash",["bash"]) - master_fd, slave_fd = pty.openpty() + while True: - proc = await asyncio.create_subprocess_exec( - "ssh", - "-o", - "StrictHostKeyChecking=no", - "-t", - f"{node['user']}@{ip}", - stdin=slave_fd, - stdout=slave_fd, - stderr=slave_fd - ) - - async def pty_to_ws(): - - fl = fcntl.fcntl(master_fd, fcntl.F_GETFL) - fcntl.fcntl(master_fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) - - while True: - - await asyncio.sleep(0.01) - - try: - data = os.read(master_fd, 1024).decode(errors="ignore") - - if data: - await websocket.send_text(data) - - except: - pass - - async def ws_to_pty(): + await asyncio.sleep(0.01) try: - while True: - - data = await websocket.receive_text() - os.write(master_fd, data.encode()) - + data=os.read(fd,1024).decode() + await websocket.send_text(data) except: pass - await asyncio.gather(pty_to_ws(), ws_to_pty()) + try: + msg = await asyncio.wait_for(websocket.receive_text(),0.01) + os.write(fd,msg.encode()) + except: + pass -if __name__ == "__main__": +# ------------------------------------------------- +# AI CHAT +# ------------------------------------------------- - import uvicorn +chat_history=[] - uvicorn.run(app, host="0.0.0.0", port=8000) +async def fake_ai(message:str): + + if "nodes" in message.lower(): + + conn=get_db() + + rows=conn.execute("SELECT name,ip,status FROM nodes").fetchall() + + conn.close() + + txt="Nodes:\n" + + for r in rows: + txt+=f"{r['name']} {r['ip']} {r['status']}\n" + + return txt + + return "AI connected." + + +@app.websocket("/ws/chat") +async def chat_ws(websocket:WebSocket): + + await websocket.accept() + + while True: + + msg = await websocket.receive_text() + + chat_history.append(msg) + + reply = await fake_ai(msg) + + await websocket.send_text(reply) + +# ------------------------------------------------- +# AI SETTINGS +# ------------------------------------------------- + +ENV_FILE = ".env" + +@app.post("/ai/settings") +async def save_settings(settings:AISettings): + + set_key(ENV_FILE,"AI_PROVIDER",settings.provider) + set_key(ENV_FILE,"OLLAMA_BASE_URL",settings.ollama) + + return {"status":"saved"} + +# ------------------------------------------------- +# ROOT +# ------------------------------------------------- + +@app.get("/") +def root(): + + return {"status":"PiDoBot running"} From 9d1e796885ea13925f208b2284923d85da9639b6 Mon Sep 17 00:00:00 2001 From: "info@pi-farm.de" Date: Fri, 6 Mar 2026 15:10:29 +0000 Subject: [PATCH 07/59] templates/index.html aktualisiert --- templates/index.html | 644 +++++++++++++++++-------------------------- 1 file changed, 250 insertions(+), 394 deletions(-) diff --git a/templates/index.html b/templates/index.html index bcb7476..c7ce251 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,405 +1,261 @@ - - -PiDoBot Dashboard + + Pi-Orchestrator AI Dashboard + + + + - - + + + + + + + + .markdown-content pre { background: #000; padding: 8px; border-radius: 4px; border: 1px solid #334155; overflow-x: auto; margin: 8px 0; } + - - -
- - - - - - - - - - - - - - - - -
- -
- - - - - - - + + +
+
🤖 KI-Orchestrator
+ +
+ + +
+ + + + + + + + + +
+
+ +
+
+
+ {% for node in nodes %} +
+ +
+
+
+ {% if node.os_type == 'debian' %} 🐧 + {% elif node.os_type == 'raspbian' %} 🍓 + {% elif node.os_type == 'fedora' %} 🎩 + {% elif node.os_type == 'ubuntu' %} 🧡 + {% else %} 💻 {% endif %} +
+ {{ node.name }} +
+
+ +
+ +
+
+
+ +
+ {{ node.ip }} + {{ node.arch }} +
+ +
+
+ + {{ node.status }} +
+ +
+ + + +
+
+ +
+ + +
+
+ {% endfor %} +
+
+ +
+
+
+
+
💬 KI Chat
+
+
+ + +
+
+
+ +
+
+
📜 System Logs
+
Warte auf Aufgaben...
+
+
+ +
+
+
🖥️ Live Terminal
+
+
+
+
+
+
+ + + + + + - + \ No newline at end of file From d9d51529155f248eb05d323f019ebd19781ee386 Mon Sep 17 00:00:00 2001 From: "info@pi-farm.de" Date: Fri, 6 Mar 2026 15:12:42 +0000 Subject: [PATCH 08/59] main.py aktualisiert --- main.py | 506 +++++++++++++++++++++++--------------------------------- 1 file changed, 210 insertions(+), 296 deletions(-) diff --git a/main.py b/main.py index 15846fc..fe011e9 100644 --- a/main.py +++ b/main.py @@ -1,327 +1,241 @@ import os -import pty -import fcntl -import subprocess -import sqlite3 -import asyncio -import re import json -import httpx -from dotenv import load_dotenv, set_key -from fastapi import FastAPI, WebSocket, WebSocketDisconnect -from fastapi.staticfiles import StaticFiles -from pydantic import BaseModel +import threading +import time +from flask import Flask, render_template, request, jsonify, redirect, url_for +from flask_sqlalchemy import SQLAlchemy +from flask_sock import Sock +import paramiko +import requests -load_dotenv() +# AI Provider Imports +import openai +import google.generativeai as genai -app = FastAPI() +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///orchestrator.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +db = SQLAlchemy(app) +sock = Sock(app) -STATIC_DIR = os.path.join(os.path.dirname(__file__),"static") -app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") +# --- DATENBANK MODELLE --- -DB_PATH = "cluster.db" -SSH_KEY = os.path.expanduser("~/.ssh/id_rsa") +class Node(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + ip = db.Column(db.String(50), nullable=False) + user = db.Column(db.String(50), nullable=False) + password = db.Column(db.String(100), nullable=False) + status = db.Column(db.String(50), default="Unbekannt") + arch = db.Column(db.String(20), default="N/A") + os_type = db.Column(db.String(50), default="linux") + has_docker = db.Column(db.Boolean, default=False) + has_vnc = db.Column(db.Boolean, default=False) -# ------------------------------------------------- -# DATABASE -# ------------------------------------------------- + def to_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} -def init_db(): - conn = sqlite3.connect(DB_PATH) +class Settings(db.Model): + id = db.Column(db.Integer, primary_key=True) + provider = db.Column(db.String(50), default="google") + google_model = db.Column(db.String(100), default="gemini-1.5-flash") + openai_model = db.Column(db.String(100), default="gpt-4o-mini") + ollama_model = db.Column(db.String(100), default="llama3") + ollama_base_url = db.Column(db.String(200), default="http://localhost:11434/v1") - 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, - arch TEXT, - docker INTEGER DEFAULT 0, - ssh INTEGER DEFAULT 1, - vnc INTEGER DEFAULT 0, - status TEXT DEFAULT 'unknown', - last_seen TEXT - ) - """) +with app.app_context(): + db.create_all() + if not Settings.query.first(): + db.session.add(Settings()) + db.session.commit() - conn.commit() - conn.close() +# --- HILFSFUNKTIONEN --- -init_db() - - -def get_db(): - conn = sqlite3.connect(DB_PATH) - conn.row_factory = sqlite3.Row - return conn - -# ------------------------------------------------- -# MODELS -# ------------------------------------------------- - -class NodeCreate(BaseModel): - name:str - ip:str - user:str - -class NodeUpdate(BaseModel): - name:str - ip:str - user:str - -class AISettings(BaseModel): - provider:str - model:str - ollama:str - -# ------------------------------------------------- -# NODE API -# ------------------------------------------------- - -@app.get("/nodes") -def get_nodes(): - - conn = get_db() - - rows = conn.execute("SELECT * FROM nodes").fetchall() - - conn.close() - - return [dict(r) for r in rows] - - -@app.post("/nodes") -def add_node(node:NodeCreate): - - conn = get_db() - - conn.execute("INSERT INTO nodes(name,ip,user) VALUES(?,?,?)", - (node.name,node.ip,node.user)) - - conn.commit() - - conn.close() - - return {"status":"ok"} - - -@app.put("/nodes/{node_id}") -def update_node(node_id:int,node:NodeUpdate): - - conn = get_db() - - conn.execute(""" - UPDATE nodes - SET name=?, ip=?, user=? - WHERE id=? - """, - (node.name,node.ip,node.user,node_id)) - - conn.commit() - - conn.close() - - return {"status":"updated"} - -# ------------------------------------------------- -# NODE SCANNING -# ------------------------------------------------- - -async def run_ssh(ip,user,cmd): - - ssh_cmd=[ - "ssh", - "-o","StrictHostKeyChecking=no", - f"{user}@{ip}", - cmd - ] - - proc = await asyncio.create_subprocess_exec( - *ssh_cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - - out,_ = await proc.communicate() - - return out.decode().strip() - - -async def detect_node(node): - - ip=node['ip'] - user=node['user'] +def get_ssh_client(node): + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + client.connect(node.ip, username=node.user, password=node.password, timeout=5) + return client +def run_ssh_cmd(node, cmd): try: - - arch = await run_ssh(ip,user,"uname -m") - - os_release = await run_ssh(ip,user,"cat /etc/os-release || uname") - - docker = await run_ssh(ip,user,"docker --version || echo nodocker") - - vnc = await run_ssh(ip,user,"pgrep Xtightvnc || echo novnc") - - os_name="unknown" - - if "debian" in os_release.lower(): - os_name="debian" - - elif "fedora" in os_release.lower(): - os_name="fedora" - - elif "raspbian" in os_release.lower(): - os_name="raspberrypi" - - elif "ubuntu" in os_release.lower(): - os_name="ubuntu" - - docker_installed = 0 if "nodocker" in docker else 1 - - vnc_enabled = 0 if "novnc" in vnc else 1 - - status="online" - + client = get_ssh_client(node) + stdin, stdout, stderr = client.exec_command(cmd) + result = stdout.read().decode().strip() + client.close() + return result except: + return None - arch="" - os_name="" - docker_installed=0 - vnc_enabled=0 - status="offline" +# --- ROUTEN --- - conn=get_db() +@app.route('/') +def index(): + nodes = Node.query.all() + return render_template('index.html', nodes=nodes) - conn.execute(""" - UPDATE nodes - SET os=?,arch=?,docker=?,vnc=?,status=?,last_seen=datetime('now') - WHERE id=? - """, - (os_name,arch,docker_installed,vnc_enabled,status,node['id'])) +@app.route('/add_node', methods=['POST']) +def add_node(): + new_node = Node( + name=request.form['name'], + ip=request.form['ip'], + user=request.form['user'], + password=request.form['password'] + ) + db.session.add(new_node) + db.session.commit() + return redirect(url_for('index')) - conn.commit() +@app.route('/edit_node/', methods=['POST']) +def edit_node(id): + node = Node.query.get_or_404(id) + node.name = request.form.get('name', node.name) + node.ip = request.form.get('ip', node.ip) + node.user = request.form.get('user', node.user) + if request.form.get('password'): + node.password = request.form.get('password') + db.session.commit() + return redirect(url_for('index')) - conn.close() +@app.route('/remove_node/', methods=['POST']) +def remove_node(id): + node = Node.query.get_or_404(id) + db.session.delete(node) + db.session.commit() + return redirect(url_for('index')) +@app.route('/refresh_status/') +def refresh_status(id): + node = Node.query.get_or_404(id) + try: + # Architektur prüfen + node.arch = run_ssh_cmd(node, "uname -m") or "N/A" + # OS Typ prüfen + os_info = run_ssh_cmd(node, "cat /etc/os-release | grep ^ID=") + node.os_type = os_info.split('=')[1].replace('"', '') if os_info else "linux" + # Docker prüfen + docker_check = run_ssh_cmd(node, "docker ps") + node.has_docker = True if docker_check is not None else False + # VNC prüfen (Port 5900/5901) + vnc_check = run_ssh_cmd(node, "netstat -tuln | grep :590") + node.has_vnc = True if vnc_check else False + + node.status = "Online" + except: + node.status = "Offline" + + db.session.commit() + return jsonify(node.to_dict()) -async def scan_nodes(): +# --- SETTINGS API --- - while True: - - conn=get_db() - - nodes=conn.execute("SELECT * FROM nodes").fetchall() - - conn.close() - - tasks=[] - - for n in nodes: - tasks.append(detect_node(n)) - - if tasks: - await asyncio.gather(*tasks) - - await asyncio.sleep(60) - - -@app.on_event("startup") -async def start_scanner(): - - asyncio.create_task(scan_nodes()) - -# ------------------------------------------------- -# TERMINAL WEBSOCKET -# ------------------------------------------------- - -@app.websocket("/ws/terminal") -async def terminal_ws(websocket:WebSocket): - - await websocket.accept() - - pid,fd = pty.fork() - - if pid==0: - os.execvp("bash",["bash"]) - - while True: - - await asyncio.sleep(0.01) +@app.route('/api/settings', methods=['GET', 'POST']) +def handle_settings(): + s = Settings.query.first() + if request.method == 'POST': + data = request.json + s.provider = data.get('provider', s.provider) + s.ollama_base_url = data.get('ollama_base_url', s.ollama_base_url) + # Speichere das Modell für den aktuellen Provider + setattr(s, f"{s.provider}_model", data.get('model')) + db.session.commit() + return jsonify({"status": "success"}) + return jsonify({ + "provider": s.provider, + "google_model": s.google_model, + "openai_model": s.openai_model, + "ollama_model": s.ollama_model, + "ollama_base_url": s.ollama_base_url + }) +@app.route('/api/models') +def get_models(): + provider = request.args.get('provider') + if provider == "google": + return jsonify({"models": ["gemini-1.5-flash", "gemini-1.5-pro"]}) + elif provider == "openai": + return jsonify({"models": ["gpt-4o-mini", "gpt-4o", "gpt-3.5-turbo"]}) + elif provider == "ollama": + url = request.args.get('url', 'http://localhost:11434/v1') try: - data=os.read(fd,1024).decode() - await websocket.send_text(data) + # Versuche Modelle von Ollama API zu laden + r = requests.get(url.replace('/v1', '/api/tags'), timeout=2) + names = [m['name'] for m in r.json().get('models', [])] + return jsonify({"models": names}) except: - pass + return jsonify({"models": ["llama3", "mistral", "codellama"]}) + return jsonify({"models": []}) - try: - msg = await asyncio.wait_for(websocket.receive_text(),0.01) - os.write(fd,msg.encode()) - except: - pass - -# ------------------------------------------------- -# AI CHAT -# ------------------------------------------------- - -chat_history=[] - -async def fake_ai(message:str): - - if "nodes" in message.lower(): - - conn=get_db() - - rows=conn.execute("SELECT name,ip,status FROM nodes").fetchall() - - conn.close() - - txt="Nodes:\n" - - for r in rows: - txt+=f"{r['name']} {r['ip']} {r['status']}\n" - - return txt - - return "AI connected." - - -@app.websocket("/ws/chat") -async def chat_ws(websocket:WebSocket): - - await websocket.accept() +# --- WEBSOCKETS --- +@sock.route('/ws/install_logs') +def install_logs(ws): + # Dummy Log Stream für Demo-Zwecke while True: + data = ws.receive(timeout=1) + # Hier könnten echte Hintergrund-Prozesse ihre Logs senden + pass - msg = await websocket.receive_text() +@sock.route('/ws/chat') +def chat_handler(ws): + s = Settings.query.first() + while True: + msg = ws.receive() + if not msg: break + + # KI LOGIK + response = "Fehler: Provider nicht konfiguriert" + try: + if s.provider == "google": + genai.configure(api_key=os.getenv("GOOGLE_API_KEY")) + model = genai.GenerativeModel(s.google_model) + response = model.generate_content(msg).text + elif s.provider == "openai": + client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY")) + res = client.chat.completions.create( + model=s.openai_model, + messages=[{"role": "user", "content": msg}] + ) + response = res.choices[0].message.content + elif s.provider == "ollama": + r = requests.post(f"{s.ollama_base_url}/chat/completions", json={ + "model": s.ollama_model, + "messages": [{"role": "user", "content": msg}], + "stream": False + }) + response = r.json()['choices'][0]['message']['content'] + except Exception as e: + response = f"KI-Fehler: {str(e)}" + + ws.send(response) - chat_history.append(msg) +@sock.route('/ws/terminal/') +def terminal_handler(ws, ip): + node = Node.query.filter_by(ip=ip).first() + if not node: return + + try: + client = get_ssh_client(node) + chan = client.invoke_shell() + + def split_reader(): + while True: + if chan.recv_ready(): + out = chan.recv(1024).decode() + ws.send(out) + time.sleep(0.01) + + threading.Thread(target=split_reader, daemon=True).start() + + while True: + cmd = ws.receive() + if not cmd: break + chan.send(cmd) + + except Exception as e: + ws.send(f"\r\n[SSH FEHLER]: {str(e)}\r\n") - reply = await fake_ai(msg) - - await websocket.send_text(reply) - -# ------------------------------------------------- -# AI SETTINGS -# ------------------------------------------------- - -ENV_FILE = ".env" - -@app.post("/ai/settings") -async def save_settings(settings:AISettings): - - set_key(ENV_FILE,"AI_PROVIDER",settings.provider) - set_key(ENV_FILE,"OLLAMA_BASE_URL",settings.ollama) - - return {"status":"saved"} - -# ------------------------------------------------- -# ROOT -# ------------------------------------------------- - -@app.get("/") -def root(): - - return {"status":"PiDoBot running"} +if __name__ == '__main__': + # Stelle sicher, dass API Keys in der Umgebung sind oder hier hartcodiert werden + # os.environ["GOOGLE_API_KEY"] = "DEIN_KEY" + app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file From 5fbd16c5ecea3d3b57f21dc9980dc59ad9202903 Mon Sep 17 00:00:00 2001 From: "info@pi-farm.de" Date: Fri, 6 Mar 2026 15:50:50 +0000 Subject: [PATCH 09/59] main.py aktualisiert --- main.py | 562 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 337 insertions(+), 225 deletions(-) diff --git a/main.py b/main.py index fe011e9..8d6ffa8 100644 --- a/main.py +++ b/main.py @@ -1,241 +1,353 @@ import os -import json -import threading -import time -from flask import Flask, render_template, request, jsonify, redirect, url_for -from flask_sqlalchemy import SQLAlchemy -from flask_sock import Sock -import paramiko -import requests - -# AI Provider Imports +import pty +import fcntl +import subprocess +import sqlite3 +import asyncio import openai -import google.generativeai as genai +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 -app = Flask(__name__) -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///orchestrator.db' -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -db = SQLAlchemy(app) -sock = Sock(app) +# Lade Umgebungsvariablen +load_dotenv() -# --- DATENBANK MODELLE --- +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") -class Node(db.Model): - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(100), nullable=False) - ip = db.Column(db.String(50), nullable=False) - user = db.Column(db.String(50), nullable=False) - password = db.Column(db.String(100), nullable=False) - status = db.Column(db.String(50), default="Unbekannt") - arch = db.Column(db.String(20), default="N/A") - os_type = db.Column(db.String(50), default="linux") - has_docker = db.Column(db.Boolean, default=False) - has_vnc = db.Column(db.Boolean, default=False) +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") - def to_dict(self): - return {c.name: getattr(self, c.name) for c in self.__table__.columns} +# 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") -class Settings(db.Model): - id = db.Column(db.Integer, primary_key=True) - provider = db.Column(db.String(50), default="google") - google_model = db.Column(db.String(100), default="gemini-1.5-flash") - openai_model = db.Column(db.String(100), default="gpt-4o-mini") - ollama_model = db.Column(db.String(100), default="llama3") - ollama_base_url = db.Column(db.String(200), default="http://localhost: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") -with app.app_context(): - db.create_all() - if not Settings.query.first(): - db.session.add(Settings()) - db.session.commit() +# --- 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() -# --- HILFSFUNKTIONEN --- +init_db() -def get_ssh_client(node): - client = paramiko.SSHClient() - client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - client.connect(node.ip, username=node.user, password=node.password, timeout=5) - return client +def get_db(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn -def run_ssh_cmd(node, cmd): - try: - client = get_ssh_client(node) - stdin, stdout, stderr = client.exec_command(cmd) - result = stdout.read().decode().strip() - client.close() - return result - except: - return None - -# --- ROUTEN --- - -@app.route('/') -def index(): - nodes = Node.query.all() - return render_template('index.html', nodes=nodes) - -@app.route('/add_node', methods=['POST']) -def add_node(): - new_node = Node( - name=request.form['name'], - ip=request.form['ip'], - user=request.form['user'], - password=request.form['password'] - ) - db.session.add(new_node) - db.session.commit() - return redirect(url_for('index')) - -@app.route('/edit_node/', methods=['POST']) -def edit_node(id): - node = Node.query.get_or_404(id) - node.name = request.form.get('name', node.name) - node.ip = request.form.get('ip', node.ip) - node.user = request.form.get('user', node.user) - if request.form.get('password'): - node.password = request.form.get('password') - db.session.commit() - return redirect(url_for('index')) - -@app.route('/remove_node/', methods=['POST']) -def remove_node(id): - node = Node.query.get_or_404(id) - db.session.delete(node) - db.session.commit() - return redirect(url_for('index')) - -@app.route('/refresh_status/') -def refresh_status(id): - node = Node.query.get_or_404(id) - try: - # Architektur prüfen - node.arch = run_ssh_cmd(node, "uname -m") or "N/A" - # OS Typ prüfen - os_info = run_ssh_cmd(node, "cat /etc/os-release | grep ^ID=") - node.os_type = os_info.split('=')[1].replace('"', '') if os_info else "linux" - # Docker prüfen - docker_check = run_ssh_cmd(node, "docker ps") - node.has_docker = True if docker_check is not None else False - # VNC prüfen (Port 5900/5901) - vnc_check = run_ssh_cmd(node, "netstat -tuln | grep :590") - node.has_vnc = True if vnc_check else False - - node.status = "Online" - except: - node.status = "Offline" +def get_system_prompt(): + conn = get_db() + nodes = conn.execute('SELECT * FROM nodes').fetchall() + conn.close() - db.session.commit() - return jsonify(node.to_dict()) - -# --- SETTINGS API --- - -@app.route('/api/settings', methods=['GET', 'POST']) -def handle_settings(): - s = Settings.query.first() - if request.method == 'POST': - data = request.json - s.provider = data.get('provider', s.provider) - s.ollama_base_url = data.get('ollama_base_url', s.ollama_base_url) - # Speichere das Modell für den aktuellen Provider - setattr(s, f"{s.provider}_model", data.get('model')) - db.session.commit() - return jsonify({"status": "success"}) - return jsonify({ - "provider": s.provider, - "google_model": s.google_model, - "openai_model": s.openai_model, - "ollama_model": s.ollama_model, - "ollama_base_url": s.ollama_base_url - }) - -@app.route('/api/models') -def get_models(): - provider = request.args.get('provider') - if provider == "google": - return jsonify({"models": ["gemini-1.5-flash", "gemini-1.5-pro"]}) - elif provider == "openai": - return jsonify({"models": ["gpt-4o-mini", "gpt-4o", "gpt-3.5-turbo"]}) - elif provider == "ollama": - url = request.args.get('url', 'http://localhost:11434/v1') - try: - # Versuche Modelle von Ollama API zu laden - r = requests.get(url.replace('/v1', '/api/tags'), timeout=2) - names = [m['name'] for m in r.json().get('models', [])] - return jsonify({"models": names}) - except: - return jsonify({"models": ["llama3", "mistral", "codellama"]}) - return jsonify({"models": []}) - -# --- WEBSOCKETS --- - -@sock.route('/ws/install_logs') -def install_logs(ws): - # Dummy Log Stream für Demo-Zwecke - while True: - data = ws.receive(timeout=1) - # Hier könnten echte Hintergrund-Prozesse ihre Logs senden - pass - -@sock.route('/ws/chat') -def chat_handler(ws): - s = Settings.query.first() - while True: - msg = ws.receive() - if not msg: break - - # KI LOGIK - response = "Fehler: Provider nicht konfiguriert" - try: - if s.provider == "google": - genai.configure(api_key=os.getenv("GOOGLE_API_KEY")) - model = genai.GenerativeModel(s.google_model) - response = model.generate_content(msg).text - elif s.provider == "openai": - client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY")) - res = client.chat.completions.create( - model=s.openai_model, - messages=[{"role": "user", "content": msg}] - ) - response = res.choices[0].message.content - elif s.provider == "ollama": - r = requests.post(f"{s.ollama_base_url}/chat/completions", json={ - "model": s.ollama_model, - "messages": [{"role": "user", "content": msg}], - "stream": False - }) - response = r.json()['choices'][0]['message']['content'] - except Exception as e: - response = f"KI-Fehler: {str(e)}" - - ws.send(response) - -@sock.route('/ws/terminal/') -def terminal_handler(ws, ip): - node = Node.query.filter_by(ip=ip).first() - if not node: return + 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" + if os.path.exists(PROMPT_FILE): + with open(PROMPT_FILE, "r", encoding="utf-8") as f: + template = f.read() + else: + template = "Du bist ein Cluster-Orchestrator. Nodes:\n{node_info}\nBefehle via cmd" + print(f"⚠️ Warnung: {PROMPT_FILE} fehlt.") + + return template.replace("{node_info}", node_info) + +# --- KI LOGIK (UNVERÄNDERT) --- +async def get_ai_response(user_input, system_prompt): + global chat_history + chat_history.append({"role": "user", "content": user_input}) + chat_history = chat_history[-30:] + ai_msg = "" try: - client = get_ssh_client(node) - chan = client.invoke_shell() - - def split_reader(): - while True: - if chan.recv_ready(): - out = chan.recv(1024).decode() - ws.send(out) - time.sleep(0.01) - - threading.Thread(target=split_reader, daemon=True).start() - - while True: - cmd = ws.receive() - if not cmd: break - chan.send(cmd) - + if AI_PROVIDER in ["openai", "ollama"]: + url = OLLAMA_BASE_URL if AI_PROVIDER == "ollama" else None + if url and not url.endswith('/v1'): url = url.rstrip('/') + '/v1' + key = "ollama" if AI_PROVIDER == "ollama" else OPENAI_API_KEY + model_to_use = OLLAMA_MODEL if AI_PROVIDER == "ollama" else OPENAI_MODEL + client = openai.OpenAI(base_url=url, api_key=key) + response = client.chat.completions.create(model=model_to_use, messages=[{"role": "system", "content": system_prompt}] + chat_history) + ai_msg = response.choices[0].message.content + elif AI_PROVIDER == "google": + client = genai.Client(api_key=GOOGLE_API_KEY) + google_history = [types.Content(role="user" if m["role"] == "user" else "model", parts=[types.Part.from_text(text=msg["content"])]) for msg in chat_history[:-1]] + chat = client.chats.create(model=GOOGLE_MODEL, config=types.GenerateContentConfig(system_instruction=system_prompt), history=google_history) + response = chat.send_message(user_input) + ai_msg = response.text except Exception as e: - ws.send(f"\r\n[SSH FEHLER]: {str(e)}\r\n") + ai_msg = f"KI Fehler: {e}" + chat_history.append({"role": "assistant", "content": ai_msg}) + return ai_msg -if __name__ == '__main__': - # Stelle sicher, dass API Keys in der Umgebung sind oder hier hartcodiert werden - # os.environ["GOOGLE_API_KEY"] = "DEIN_KEY" - app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file +# --- WebSocket Manager --- +class ConnectionManager: + def __init__(self): self.active_connections = [] + async def connect(self, ws: WebSocket): await ws.accept(); self.active_connections.append(ws) + def disconnect(self, ws: WebSocket): self.active_connections.remove(ws) + async def broadcast(self, msg: str): + for c in self.active_connections: + try: await c.send_text(msg) + except: pass +manager = ConnectionManager() + +# --- 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) + + 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}) + +@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 {} + +@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) + +@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) + +@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) + +@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"} + + 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) + +@app.websocket("/ws/terminal/{ip}") +async def terminal_websocket(websocket: WebSocket, ip: str): + await websocket.accept() + conn = get_db(); node = conn.execute('SELECT * FROM nodes WHERE ip = ?', (ip,)).fetchone(); conn.close() + if not node: await websocket.close(); return + master_fd, slave_fd = pty.openpty() + proc = await asyncio.create_subprocess_exec("ssh", "-o", "StrictHostKeyChecking=no", "-t", f"{node['user']}@{ip}", stdin=slave_fd, stdout=slave_fd, stderr=slave_fd) + async def pty_to_ws(): + fl = fcntl.fcntl(master_fd, fcntl.F_GETFL); fcntl.fcntl(master_fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) + try: + while True: + await asyncio.sleep(0.01) + try: + data = os.read(master_fd, 1024).decode(errors='ignore') + if data: await websocket.send_text(data) + except BlockingIOError: continue + except: pass + async def ws_to_pty(): + try: + while True: + data = await websocket.receive_text(); os.write(master_fd, data.encode()) + except: pass + try: await asyncio.gather(pty_to_ws(), ws_to_pty()) + finally: + if proc.returncode is None: proc.terminate() + os.close(master_fd); os.close(slave_fd) + +@app.websocket("/ws/chat") +async def chat_endpoint(websocket: WebSocket): + await websocket.accept() + try: + while True: + user_msg = await websocket.receive_text() + ai_response = await get_ai_response(user_msg, get_system_prompt()) + commands = re.findall(r'(.*?)', ai_response, re.I | re.S) + clean_msg = re.sub(r'.*?', '', ai_response, flags=re.I | re.S).strip() + if clean_msg: await websocket.send_text(clean_msg) + if commands: + tasks = [] + for target, cmd in commands: + conn = get_db(); n = conn.execute('SELECT * FROM nodes WHERE ip=? OR name=?', (target.strip(), target.strip())).fetchone(); conn.close() + if n: tasks.append(run_remote_task(n['ip'], n['user'], cmd.strip())) + if tasks: + await websocket.send_text("ℹ️ *Führe Befehle aus...*") + await asyncio.gather(*tasks) + summary = await get_ai_response("Zusammenfassung der Ergebnisse?", get_system_prompt()) + await websocket.send_text(f"--- Info ---\n{summary}") + except: pass + +async def run_remote_task(ip, user, cmd): + await manager.broadcast(f"🚀 Task: {cmd} auf {ip}") + proc = await asyncio.create_subprocess_shell(f"ssh -o StrictHostKeyChecking=no {user}@{ip} '{cmd}'", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) + full_output = "" + while True: + line = await proc.stdout.readline() + if not line: break + out = line.decode('utf-8', errors='ignore').strip() + if out: await manager.broadcast(f"🛠️ {out}"); full_output += out + "\n" + await proc.wait() + chat_history.append({"role": "user", "content": f"[SYSTEM] Befehl '{cmd}' auf {ip} fertig:\n{full_output or 'Kein Output'}"}) + +# --- Settings API --- +@app.get("/api/settings") +async def get_settings(): + return {"provider": AI_PROVIDER, "google_model": GOOGLE_MODEL, "openai_model": OPENAI_MODEL, "ollama_model": OLLAMA_MODEL, "ollama_base_url": OLLAMA_BASE_URL} + +@app.post("/api/settings") +async def update_settings(request: Request): + global AI_PROVIDER, GOOGLE_MODEL, OPENAI_MODEL, OLLAMA_MODEL, OLLAMA_BASE_URL + data = await request.json() + provider = data.get("provider") + if provider: + AI_PROVIDER = provider; set_key(ENV_FILE, "AI_PROVIDER", provider) + if data.get("model"): + m = data.get("model") + if provider == "google": GOOGLE_MODEL = m; set_key(ENV_FILE, "GOOGLE_MODEL", m) + if provider == "openai": OPENAI_MODEL = m; set_key(ENV_FILE, "OPENAI_MODEL", m) + if provider == "ollama": OLLAMA_MODEL = m; set_key(ENV_FILE, "OLLAMA_MODEL", m) + if data.get("ollama_base_url"): + u = data.get("ollama_base_url"); OLLAMA_BASE_URL = u; set_key(ENV_FILE, "OLLAMA_BASE_URL", u) + return {"status": "success"} + +@app.get("/api/models") +async def get_models(provider: str): + try: + if provider == "ollama": + url = OLLAMA_BASE_URL.replace("/v1", "").rstrip("/") + async with httpx.AsyncClient() as client: + r = await client.get(f"{url}/api/tags", timeout=5.0) + return {"models": [m["name"] for m in r.json().get("models", [])]} + elif provider == "openai": + client = openai.AsyncOpenAI(api_key=OPENAI_API_KEY) + r = await client.models.list() + return {"models": sorted([m.id for m in r.data if "gpt" in m.id or "o1" in m.id])} + elif provider == "google": + client = genai.Client(api_key=GOOGLE_API_KEY) + return {"models": sorted([m.name.replace("models/", "") for m in client.models.list() if 'generateContent' in m.supported_actions])} + except: return {"models": []} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file From 3d5504b5e8c0fc7402918aa7656749d1f8cc0a7a Mon Sep 17 00:00:00 2001 From: "info@pi-farm.de" Date: Fri, 6 Mar 2026 15:52:07 +0000 Subject: [PATCH 10/59] main.py aktualisiert --- main.py | 744 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 413 insertions(+), 331 deletions(-) diff --git a/main.py b/main.py index 8d6ffa8..26b5441 100644 --- a/main.py +++ b/main.py @@ -1,353 +1,435 @@ -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() + + + + + Pi-Orchestrator AI Dashboard + - 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" + + + + + + + - if os.path.exists(PROMPT_FILE): - with open(PROMPT_FILE, "r", encoding="utf-8") as f: - template = f.read() - else: - template = "Du bist ein Cluster-Orchestrator. Nodes:\n{node_info}\nBefehle via cmd" - print(f"⚠️ Warnung: {PROMPT_FILE} fehlt.") - - return template.replace("{node_info}", node_info) - -# --- KI LOGIK (UNVERÄNDERT) --- -async def get_ai_response(user_input, system_prompt): - global chat_history - chat_history.append({"role": "user", "content": user_input}) - chat_history = chat_history[-30:] - ai_msg = "" - try: - if AI_PROVIDER in ["openai", "ollama"]: - url = OLLAMA_BASE_URL if AI_PROVIDER == "ollama" else None - if url and not url.endswith('/v1'): url = url.rstrip('/') + '/v1' - key = "ollama" if AI_PROVIDER == "ollama" else OPENAI_API_KEY - model_to_use = OLLAMA_MODEL if AI_PROVIDER == "ollama" else OPENAI_MODEL - client = openai.OpenAI(base_url=url, api_key=key) - response = client.chat.completions.create(model=model_to_use, messages=[{"role": "system", "content": system_prompt}] + chat_history) - ai_msg = response.choices[0].message.content - elif AI_PROVIDER == "google": - client = genai.Client(api_key=GOOGLE_API_KEY) - google_history = [types.Content(role="user" if m["role"] == "user" else "model", parts=[types.Part.from_text(text=msg["content"])]) for msg in chat_history[:-1]] - chat = client.chats.create(model=GOOGLE_MODEL, config=types.GenerateContentConfig(system_instruction=system_prompt), history=google_history) - response = chat.send_message(user_input) - ai_msg = response.text - except Exception as e: - ai_msg = f"KI Fehler: {e}" - chat_history.append({"role": "assistant", "content": ai_msg}) - return ai_msg - -# --- WebSocket Manager --- -class ConnectionManager: - def __init__(self): self.active_connections = [] - async def connect(self, ws: WebSocket): await ws.accept(); self.active_connections.append(ws) - def disconnect(self, ws: WebSocket): self.active_connections.remove(ws) - async def broadcast(self, msg: str): - for c in self.active_connections: - try: await c.send_text(msg) - except: pass -manager = ConnectionManager() - -# --- 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) - - 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" + + + - 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)" +
+
🤖 KI-Orchestrator
- 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) +
-@app.websocket("/ws/terminal/{ip}") -async def terminal_websocket(websocket: WebSocket, ip: str): - await websocket.accept() - conn = get_db(); node = conn.execute('SELECT * FROM nodes WHERE ip = ?', (ip,)).fetchone(); conn.close() - if not node: await websocket.close(); return - master_fd, slave_fd = pty.openpty() - proc = await asyncio.create_subprocess_exec("ssh", "-o", "StrictHostKeyChecking=no", "-t", f"{node['user']}@{ip}", stdin=slave_fd, stdout=slave_fd, stderr=slave_fd) - async def pty_to_ws(): - fl = fcntl.fcntl(master_fd, fcntl.F_GETFL); fcntl.fcntl(master_fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) - try: - while True: - await asyncio.sleep(0.01) - try: - data = os.read(master_fd, 1024).decode(errors='ignore') - if data: await websocket.send_text(data) - except BlockingIOError: continue - except: pass - async def ws_to_pty(): - try: - while True: - data = await websocket.receive_text(); os.write(master_fd, data.encode()) - except: pass - try: await asyncio.gather(pty_to_ws(), ws_to_pty()) - finally: - if proc.returncode is None: proc.terminate() - os.close(master_fd); os.close(slave_fd) + + + + + + +
+ + +
+ + + +
+
-@app.websocket("/ws/chat") -async def chat_endpoint(websocket: WebSocket): - await websocket.accept() - try: - while True: - user_msg = await websocket.receive_text() - ai_response = await get_ai_response(user_msg, get_system_prompt()) - commands = re.findall(r'(.*?)', ai_response, re.I | re.S) - clean_msg = re.sub(r'.*?', '', ai_response, flags=re.I | re.S).strip() - if clean_msg: await websocket.send_text(clean_msg) - if commands: - tasks = [] - for target, cmd in commands: - conn = get_db(); n = conn.execute('SELECT * FROM nodes WHERE ip=? OR name=?', (target.strip(), target.strip())).fetchone(); conn.close() - if n: tasks.append(run_remote_task(n['ip'], n['user'], cmd.strip())) - if tasks: - await websocket.send_text("ℹ️ *Führe Befehle aus...*") - await asyncio.gather(*tasks) - summary = await get_ai_response("Zusammenfassung der Ergebnisse?", get_system_prompt()) - await websocket.send_text(f"--- Info ---\n{summary}") - except: pass +
+
+
+ {% for node in nodes %} +
+
+
{{ node.name }}
+
+ +
+
+ +
+ {{ node.ip }} + +
-async def run_remote_task(ip, user, cmd): - await manager.broadcast(f"🚀 Task: {cmd} auf {ip}") - proc = await asyncio.create_subprocess_shell(f"ssh -o StrictHostKeyChecking=no {user}@{ip} '{cmd}'", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) - full_output = "" - while True: - line = await proc.stdout.readline() - if not line: break - out = line.decode('utf-8', errors='ignore').strip() - if out: await manager.broadcast(f"🛠️ {out}"); full_output += out + "\n" - await proc.wait() - chat_history.append({"role": "user", "content": f"[SYSTEM] Befehl '{cmd}' auf {ip} fertig:\n{full_output or 'Kein Output'}"}) +
+ {{ node.os }} + {{ node.arch }} +
-# --- Settings API --- -@app.get("/api/settings") -async def get_settings(): - return {"provider": AI_PROVIDER, "google_model": GOOGLE_MODEL, "openai_model": OPENAI_MODEL, "ollama_model": OLLAMA_MODEL, "ollama_base_url": OLLAMA_BASE_URL} +
+ + {{ node.status }} +
+ +
+ {% endfor %} +
+ +
-@app.post("/api/settings") -async def update_settings(request: Request): - global AI_PROVIDER, GOOGLE_MODEL, OPENAI_MODEL, OLLAMA_MODEL, OLLAMA_BASE_URL - data = await request.json() - provider = data.get("provider") - if provider: - AI_PROVIDER = provider; set_key(ENV_FILE, "AI_PROVIDER", provider) - if data.get("model"): - m = data.get("model") - if provider == "google": GOOGLE_MODEL = m; set_key(ENV_FILE, "GOOGLE_MODEL", m) - if provider == "openai": OPENAI_MODEL = m; set_key(ENV_FILE, "OPENAI_MODEL", m) - if provider == "ollama": OLLAMA_MODEL = m; set_key(ENV_FILE, "OLLAMA_MODEL", m) - if data.get("ollama_base_url"): - u = data.get("ollama_base_url"); OLLAMA_BASE_URL = u; set_key(ENV_FILE, "OLLAMA_BASE_URL", u) - return {"status": "success"} +
+
+
+
+
💬 KI Chat
+
+
+ + +
+
+
-@app.get("/api/models") -async def get_models(provider: str): - try: - if provider == "ollama": - url = OLLAMA_BASE_URL.replace("/v1", "").rstrip("/") - async with httpx.AsyncClient() as client: - r = await client.get(f"{url}/api/tags", timeout=5.0) - return {"models": [m["name"] for m in r.json().get("models", [])]} - elif provider == "openai": - client = openai.AsyncOpenAI(api_key=OPENAI_API_KEY) - r = await client.models.list() - return {"models": sorted([m.id for m in r.data if "gpt" in m.id or "o1" in m.id])} - elif provider == "google": - client = genai.Client(api_key=GOOGLE_API_KEY) - return {"models": sorted([m.name.replace("models/", "") for m in client.models.list() if 'generateContent' in m.supported_actions])} - except: return {"models": []} +
+
+
📜 System Logs
+
Warte auf Aufgaben...
+
+
-if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file +
+
+
🖥️ Live Terminal
+
+
+
+
+
+
+ + + + + + \ No newline at end of file From 6845b1bea9be25e1c2b7d305c060521c37779ef1 Mon Sep 17 00:00:00 2001 From: "info@pi-farm.de" Date: Fri, 6 Mar 2026 15:59:34 +0000 Subject: [PATCH 11/59] main.py aktualisiert --- main.py | 740 +++++++++++++++++++++++++------------------------------- 1 file changed, 329 insertions(+), 411 deletions(-) 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 From 269ef7992c845262cb49f7ab5eefce4f8dbad3d7 Mon Sep 17 00:00:00 2001 From: "info@pi-farm.de" Date: Fri, 6 Mar 2026 15:59:59 +0000 Subject: [PATCH 12/59] templates/index.html aktualisiert --- templates/index.html | 448 ++++++++++++++++++++++++++++++------------- 1 file changed, 311 insertions(+), 137 deletions(-) diff --git a/templates/index.html b/templates/index.html index c7ce251..26b5441 100644 --- a/templates/index.html +++ b/templates/index.html @@ -12,7 +12,6 @@ -
-
🤖 KI-Orchestrator
+
🤖 KI-Orchestrator
-
- -
+
- - + + -
-
-
+
+
{% for node in nodes %} -
+
+
+
{{ node.name }}
+
+ +
+
-
-
-
- {% if node.os_type == 'debian' %} 🐧 - {% elif node.os_type == 'raspbian' %} 🍓 - {% elif node.os_type == 'fedora' %} 🎩 - {% elif node.os_type == 'ubuntu' %} 🧡 - {% else %} 💻 {% endif %} -
- {{ node.name }} -
-
- -
- -
-
+
+ {{ node.ip }} +
-
- {{ node.ip }} - {{ node.arch }} +
+ {{ node.os }} + {{ node.arch }}
-
-
- - {{ node.status }} -
- -
- - - -
-
- -
- - +
+ + {{ node.status }}
+
{% endfor %}
+
-
+
💬 KI Chat
- - + +
-
+
📜 System Logs
Warte auf Aufgaben...
-
+
🖥️ Live Terminal
@@ -176,86 +254,182 @@
-