Files
app-installer/app.py
SimolZimol 0f83f15588 new file: README.md
new file:   app.py
	new file:   config.example.json
	new file:   config.json
	new file:   docker_diagnose.py
	new file:   requirements.txt
	new file:   start.bat
	new file:   templates/available_projects.html
	new file:   templates/base.html
	new file:   templates/config.html
	new file:   templates/docker_status.html
	new file:   templates/index.html
	new file:   templates/project_details.html
	new file:   templates/project_details_fixed.html
	new file:   templates/project_details_new.html
	new file:   templates/project_details_old.html
	.gitignore
2025-07-04 23:50:04 +02:00

1353 lines
55 KiB
Python

from flask import Flask, render_template, request, jsonify, flash, redirect, url_for
import requests
import os
import subprocess
import json
import re
from urllib.parse import urlparse
import docker
import yaml
from datetime import datetime
app = Flask(__name__)
app.secret_key = 'your-secret-key-change-this'
# Configuration
CONFIG_FILE = 'config.json'
PROJECTS_DIR = 'projects'
APPS_DIR = 'apps'
class ProjectManager:
def __init__(self):
self.docker_client = None
self.docker_available = False
try:
self.docker_client = docker.from_env()
# Teste Docker-Verbindung
self.docker_client.ping()
self.docker_available = True
print("✓ Docker erfolgreich verbunden")
except docker.errors.DockerException as e:
print(f"Docker-Verbindungsfehler: {e}")
self.docker_client = None
except Exception as e:
print(f"Docker nicht verfügbar: {e}")
self.docker_client = None
# Erstelle notwendige Verzeichnisse
os.makedirs(PROJECTS_DIR, exist_ok=True)
os.makedirs(APPS_DIR, exist_ok=True)
def load_config(self):
"""Lade Konfiguration aus config.json"""
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return {
'project_list_url': '',
'auto_refresh_minutes': 30,
'docker_registry': '',
'projects': []
}
def save_config(self, config):
"""Speichere Konfiguration in config.json"""
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
def fetch_project_list(self, url):
"""Hole Projektliste von einer URL"""
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
# Versuche JSON zu parsen
try:
return response.json()
except:
# Falls kein JSON, extrahiere URLs aus HTML/Text
urls = re.findall(r'https?://[^\s<>"]+', response.text)
git_urls = [url for url in urls if any(domain in url for domain in ['github.com', 'gitlab.com', 'gitea'])]
return [{'url': url, 'name': self.extract_project_name(url)} for url in git_urls]
except Exception as e:
print(f"Fehler beim Abrufen der Projektliste: {e}")
return []
def extract_project_name(self, url):
"""Extrahiere Projektname aus URL"""
path = urlparse(url).path
parts = path.strip('/').split('/')
return parts[-1] if parts else 'unknown'
def clone_project(self, project_url, project_name):
"""Klone Projekt aus Git-Repository"""
project_path = os.path.join(PROJECTS_DIR, project_name)
if os.path.exists(project_path):
return False, f"Projekt {project_name} existiert bereits"
try:
result = subprocess.run([
'git', 'clone', project_url, project_path
], capture_output=True, text=True, timeout=300)
if result.returncode == 0:
return True, f"Projekt {project_name} erfolgreich geklont"
else:
return False, f"Fehler beim Klonen: {result.stderr}"
except subprocess.TimeoutExpired:
return False, "Timeout beim Klonen des Projekts"
except Exception as e:
return False, f"Fehler beim Klonen: {str(e)}"
def get_project_info(self, project_name):
"""Hole Projektinformationen"""
project_path = os.path.join(PROJECTS_DIR, project_name)
if not os.path.exists(project_path):
return None
info = {
'name': project_name,
'path': project_path,
'has_dockerfile': os.path.exists(os.path.join(project_path, 'Dockerfile')),
'has_env_example': os.path.exists(os.path.join(project_path, '.env.example')),
'has_docker_compose': os.path.exists(os.path.join(project_path, 'docker-compose.yml')),
'status': self.get_container_status(project_name),
'created': datetime.fromtimestamp(os.path.getctime(project_path)).strftime('%Y-%m-%d %H:%M')
}
# Lese README falls vorhanden
readme_files = ['README.md', 'readme.md', 'README.txt', 'readme.txt']
for readme in readme_files:
readme_path = os.path.join(project_path, readme)
if os.path.exists(readme_path):
with open(readme_path, 'r', encoding='utf-8', errors='ignore') as f:
info['readme'] = f.read()[:1000] # Ersten 1000 Zeichen
break
return info
def get_container_status(self, project_name):
"""Prüfe Container-Status"""
if not self.docker_available or not self.docker_client:
return 'docker_unavailable'
try:
containers = self.docker_client.containers.list(all=True, filters={'name': project_name})
if containers:
return containers[0].status
return 'not_created'
except docker.errors.DockerException as e:
print(f"Docker-Fehler beim Status-Check: {e}")
return 'docker_error'
except Exception as e:
print(f"Unerwarteter Fehler beim Status-Check: {e}")
return 'unknown'
def build_project(self, project_name):
"""Baue Docker-Image für Projekt"""
project_path = os.path.join(PROJECTS_DIR, project_name)
if not os.path.exists(os.path.join(project_path, 'Dockerfile')):
return False, "Kein Dockerfile gefunden"
if not self.docker_available:
return False, "Docker ist nicht verfügbar. Bitte starten Sie Docker Desktop."
try:
# Kopiere .env.example zu .env falls nicht vorhanden
env_example = os.path.join(project_path, '.env.example')
env_file = os.path.join(project_path, '.env')
if os.path.exists(env_example) and not os.path.exists(env_file):
with open(env_example, 'r', encoding='utf-8') as src, open(env_file, 'w', encoding='utf-8') as dst:
dst.write(src.read())
# Docker Build
print(f"Building Docker image for {project_name}...")
# Analysiere Dockerfile vor dem Build
dockerfile_ports = self.analyze_dockerfile_ports(project_path)
if dockerfile_ports:
print(f"Dockerfile exponiert Ports: {dockerfile_ports}")
image, logs = self.docker_client.images.build(
path=project_path,
tag=f"{project_name}:latest",
rm=True
)
# Log build output for debugging
for log in logs:
if 'stream' in log:
print(log['stream'].strip())
# Analysiere Image nach dem Build
built_ports = self.detect_container_exposed_ports(project_name)
port_info = ""
if built_ports:
port_info = f" (Exponierte Ports: {', '.join(built_ports)})"
return True, f"Image {project_name}:latest erfolgreich gebaut{port_info}"
except docker.errors.BuildError as e:
error_msg = "Build-Fehler:\n"
for log in e.build_log:
if 'stream' in log:
error_msg += log['stream']
return False, error_msg
except docker.errors.DockerException as e:
return False, f"Docker-Fehler beim Build: {str(e)}"
except Exception as e:
return False, f"Unerwarteter Fehler beim Build: {str(e)}"
def check_port_availability(self, port):
"""Prüfe ob ein Port verfügbar ist"""
import socket
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(1)
result = sock.connect_ex(('localhost', int(port)))
return result != 0 # Port ist frei wenn Verbindung fehlschlägt
except:
return False
def find_available_port(self, start_port=8080, max_attempts=50):
"""Finde einen verfügbaren Port beginnend ab start_port"""
for port in range(int(start_port), int(start_port) + max_attempts):
if self.check_port_availability(port):
return port
return None
def check_container_health(self, container, port):
"""Prüfe ob Container tatsächlich erreichbar ist"""
import requests
import time
max_attempts = 10
for attempt in range(max_attempts):
try:
# Prüfe Container-Status
container.reload()
if container.status != 'running':
return False, f"Container nicht am Laufen (Status: {container.status})"
# Prüfe HTTP-Erreichbarkeit wenn Port definiert
if port:
try:
response = requests.get(f"http://localhost:{port}", timeout=2)
if response.status_code < 500: # Akzeptiere alle Antworten außer Server-Errors
return True, "Container erreichbar"
except requests.exceptions.RequestException:
pass # HTTP noch nicht bereit, aber das ist OK in den ersten Sekunden
time.sleep(1)
except Exception as e:
return False, f"Health Check Fehler: {str(e)}"
return True, "Container läuft (HTTP-Check übersprungen)"
def analyze_app_files_for_ports(self, project_path):
"""Analysiere App-Dateien um verwendete Ports zu finden"""
ports = []
# Typische Dateien durchsuchen
files_to_check = ['app.py', 'main.py', 'server.py', 'index.js', 'package.json']
for filename in files_to_check:
filepath = os.path.join(project_path, filename)
if os.path.exists(filepath):
try:
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
# Suche nach Port-Definitionen
import re
# Flask/Python Patterns
flask_patterns = [
r'app\.run\([^)]*port\s*=\s*(\d+)',
r'host\s*=\s*["\'][^"\']*["\'],?\s*port\s*=\s*(\d+)',
r'port\s*=\s*(\d+)',
r'PORT\s*=\s*(\d+)',
]
# Node.js Patterns
node_patterns = [
r'listen\s*\(\s*(\d+)',
r'port\s*[:=]\s*(\d+)',
r'process\.env\.PORT\s*\|\|\s*(\d+)',
]
all_patterns = flask_patterns + node_patterns
for pattern in all_patterns:
matches = re.findall(pattern, content, re.IGNORECASE)
for match in matches:
port_num = int(match)
if 1 <= port_num <= 65535: # Gültiger Port-Bereich
port_spec = f"{port_num}/tcp"
if port_spec not in ports:
ports.append(port_spec)
print(f"Found port {port_num} in {filename}")
except Exception as e:
print(f"Warning: Could not analyze {filename}: {e}")
return ports
def analyze_dockerfile_ports(self, project_path):
"""Analysiere Dockerfile um exponierte Ports zu finden"""
dockerfile_path = os.path.join(project_path, 'Dockerfile')
ports = []
if os.path.exists(dockerfile_path):
try:
with open(dockerfile_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip().upper()
if line.startswith('EXPOSE'):
# EXPOSE 8080 oder EXPOSE 8080/tcp
parts = line.split()
for part in parts[1:]: # Skip "EXPOSE"
if part.isdigit():
ports.append(f"{part}/tcp")
elif '/' in part:
ports.append(part)
except Exception as e:
print(f"Warning: Could not analyze Dockerfile: {e}")
return ports
def detect_container_exposed_ports(self, image_name):
"""Erkenne welche Ports das Container-Image exponiert"""
try:
if not self.docker_available:
return []
image = self.docker_client.images.get(f"{image_name}:latest")
config = image.attrs.get('Config', {})
exposed_ports = config.get('ExposedPorts', {})
ports = []
for port_spec in exposed_ports.keys():
ports.append(port_spec)
# Falls keine Ports im Image exponiert sind, analysiere Projekt-Dateien
if not ports:
print(f"Keine Ports im Image {image_name} exponiert, analysiere Projekt-Dateien...")
project_path = os.path.join(PROJECTS_DIR, image_name)
# Analysiere Dockerfile und App-Dateien
dockerfile_ports = self.analyze_dockerfile_ports(project_path)
app_ports = self.analyze_app_files_for_ports(project_path)
# Kombiniere gefundene Ports
all_found_ports = dockerfile_ports + app_ports
if all_found_ports:
ports = all_found_ports
print(f"Ports aus Projekt-Analyse gefunden: {ports}")
else:
# Fallback zu Standard-Ports
ports = ['5000/tcp', '8080/tcp', '80/tcp', '3000/tcp']
print(f"Keine Ports gefunden, verwende Fallback: {ports}")
else:
print(f"Image {image_name} exponiert Ports: {ports}")
return ports
except Exception as e:
print(f"Warning: Could not detect exposed ports for {image_name}: {e}")
# Als letzter Fallback, analysiere Projekt-Dateien
try:
project_path = os.path.join(PROJECTS_DIR, image_name)
app_ports = self.analyze_app_files_for_ports(project_path)
if app_ports:
print(f"Fallback: Ports aus App-Dateien: {app_ports}")
return app_ports
except:
pass
# Ultimativer Fallback
return ['5000/tcp', '8080/tcp', '80/tcp', '3000/tcp']
def create_smart_port_mapping(self, project_name, host_port):
"""Erstelle intelligentes Port-Mapping basierend auf Image-Konfiguration"""
try:
exposed_ports = self.detect_container_exposed_ports(project_name)
if not exposed_ports:
print(f"Keine Ports im Image {project_name} gefunden, verwende Fallback")
# Verwende nur einen Standard-Port
return {'8080/tcp': host_port}
# Wenn nur ein Port exponiert ist, verwende diesen
if len(exposed_ports) == 1:
return {exposed_ports[0]: host_port}
# Wenn mehrere Ports exponiert sind, priorisiere Standard-Web-Ports
web_ports = ['80/tcp', '8080/tcp', '3000/tcp', '5000/tcp']
for web_port in web_ports:
if web_port in exposed_ports:
print(f"Verwende priorisierten Web-Port: {web_port} -> {host_port}")
return {web_port: host_port}
# Falls kein Standard-Web-Port, verwende den ersten
first_port = exposed_ports[0]
print(f"Verwende ersten exponierten Port: {first_port} -> {host_port}")
return {first_port: host_port}
except Exception as e:
print(f"Error creating port mapping: {e}")
# Fallback
return {'8080/tcp': host_port}
def wait_for_container_ready(self, container, port=None, timeout=30):
"""Warte bis Container bereit ist oder Timeout erreicht"""
import time
start_time = time.time()
while time.time() - start_time < timeout:
try:
container.reload()
if container.status == 'running':
# Container läuft - mache zusätzlichen Health Check
if port:
health_ok, health_msg = self.check_container_health(container, port)
if health_ok:
return True, "Container läuft und ist erreichbar"
# Falls Health Check fehlschlägt, warte noch etwas
else:
return True, "Container läuft"
elif container.status in ['exited', 'dead']:
logs = container.logs(tail=20).decode('utf-8', errors='ignore')
return False, f"Container beendet mit Status '{container.status}'. Logs: {logs[:300]}"
time.sleep(1)
except Exception as e:
return False, f"Fehler beim Prüfen des Container-Status: {str(e)}"
# Timeout erreicht - prüfe finalen Status
try:
container.reload()
if container.status == 'running':
return True, f"Container läuft (Timeout nach {timeout}s erreicht, aber Container läuft)"
except:
pass
return False, f"Timeout beim Warten auf Container-Start (>{timeout}s)"
def start_project(self, project_name, port=None):
"""Starte Projekt-Container"""
if not self.docker_available:
return False, "Docker ist nicht verfügbar. Bitte starten Sie Docker Desktop."
try:
# Stoppe existierenden Container falls vorhanden
try:
existing = self.docker_client.containers.get(project_name)
print(f"Stopping existing container {project_name}")
existing.stop()
existing.remove()
except docker.errors.NotFound:
pass # Container existiert nicht, das ist OK
# Prüfe ob Image existiert
try:
self.docker_client.images.get(f"{project_name}:latest")
except docker.errors.ImageNotFound:
return False, f"Image {project_name}:latest nicht gefunden. Bitte bauen Sie das Projekt zuerst."
# Port-Verwaltung verbessert
if port:
port = int(port)
if not self.check_port_availability(port):
# Suche alternativen Port
alternative_port = self.find_available_port(port)
if alternative_port:
return False, f"Port {port} ist bereits belegt. Verfügbarer alternativer Port: {alternative_port}. Möchten Sie Port {alternative_port} verwenden?"
else:
return False, f"Port {port} ist bereits belegt und keine Alternative zwischen {port}-{port+50} gefunden."
else:
# Finde automatisch einen freien Port
port = self.find_available_port()
if not port:
return False, "Kein freier Port zwischen 8080-8130 gefunden."
# Intelligentes Port-Mapping basierend auf Image-Konfiguration
ports = {}
if port:
ports = self.create_smart_port_mapping(project_name, port)
# Umgebungsvariablen laden
env_vars = {}
project_path = os.path.join(PROJECTS_DIR, project_name)
env_file = os.path.join(project_path, '.env')
if os.path.exists(env_file):
try:
with open(env_file, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
env_vars[key.strip()] = value.strip()
except Exception as e:
print(f"Warning: Could not load .env file: {e}")
print(f"Starting container {project_name} with ports: {ports}")
print(f"Environment variables: {env_vars}")
# Erstelle Container (aber starte noch nicht)
container = self.docker_client.containers.create(
f"{project_name}:latest",
name=project_name,
ports=ports if ports else None,
environment=env_vars,
restart_policy={"Name": "unless-stopped"},
detach=True
)
print(f"Container {project_name} created with ID: {container.id}")
# Starte Container explizit
container.start()
print(f"Container {project_name} started")
# Warte auf Container-Bereitschaft mit Timeout
success, status_msg = self.wait_for_container_ready(container, port, timeout=30)
if success:
return True, f"Container {project_name} erfolgreich gestartet auf Port {port}. Zugriff über: http://localhost:{port}"
else:
# Container-Start fehlgeschlagen - cleanup
try:
container.stop(timeout=5)
container.remove()
except Exception as cleanup_error:
print(f"Cleanup error: {cleanup_error}")
return False, f"Container-Start fehlgeschlagen: {status_msg}"
except docker.errors.ContainerError as e:
return False, f"Container-Laufzeitfehler: {str(e)}"
except docker.errors.ImageNotFound as e:
return False, f"Image nicht gefunden: {str(e)}. Bitte bauen Sie das Projekt zuerst."
except docker.errors.APIError as e:
error_msg = str(e)
if "port is already allocated" in error_msg:
# Extrahiere Port aus Fehlermeldung
import re
port_match = re.search(r':(\d+)', error_msg)
if port_match:
blocked_port = port_match.group(1)
# Suche alternativen Port
alternative_port = self.find_available_port(int(blocked_port) + 1)
if alternative_port:
return False, f"Port {blocked_port} ist bereits belegt. Alternativer freier Port: {alternative_port}"
else:
return False, f"Port {blocked_port} ist bereits belegt und keine Alternative gefunden."
else:
return False, f"Port-Konflikt: {error_msg}"
else:
return False, f"Docker-API-Fehler: {error_msg}"
except Exception as e:
return False, f"Unerwarteter Fehler beim Starten: {str(e)}"
def stop_project(self, project_name):
"""Stoppe Projekt-Container"""
try:
if self.docker_client:
container = self.docker_client.containers.get(project_name)
container.stop()
return True, f"Container {project_name} gestoppt"
else:
return False, "Docker Client nicht verfügbar"
except Exception as e:
return False, f"Fehler beim Stoppen: {str(e)}"
def force_remove_directory(self, path):
"""Entferne Verzeichnis robust unter Windows (behandelt schreibgeschützte Dateien)"""
import stat
import shutil
def handle_remove_readonly(func, path, exc):
"""Behandle schreibgeschützte Dateien unter Windows"""
try:
# Entferne readonly-Attribut und versuche erneut
os.chmod(path, stat.S_IWRITE)
func(path)
except Exception as e:
print(f"Warning: Could not remove {path}: {e}")
try:
if os.path.exists(path):
print(f"Removing directory: {path}")
shutil.rmtree(path, onerror=handle_remove_readonly)
print(f"Successfully removed: {path}")
return True
except Exception as e:
print(f"Error removing directory {path}: {e}")
# Fallback: Versuche mit Windows-Kommandos
try:
import subprocess
# Windows rmdir mit Force-Parameter
result = subprocess.run(['cmd', '/c', 'rmdir', '/s', '/q', path],
capture_output=True, text=True, timeout=30)
if result.returncode == 0:
print(f"Successfully removed with Windows rmdir: {path}")
return True
else:
print(f"Windows rmdir failed: {result.stderr}")
except Exception as fallback_error:
print(f"Fallback removal failed: {fallback_error}")
return False
return True
def remove_project(self, project_name):
"""Entferne Projekt komplett"""
try:
print(f"Starting removal of project: {project_name}")
# Stoppe und entferne Container
if self.docker_client:
try:
container = self.docker_client.containers.get(project_name)
print(f"Stopping container {project_name}")
container.stop(timeout=10)
container.remove()
print(f"Container {project_name} removed")
except docker.errors.NotFound:
print(f"Container {project_name} not found (already removed)")
except Exception as container_error:
print(f"Warning: Container removal failed: {container_error}")
# Entferne Image
try:
print(f"Removing image {project_name}:latest")
self.docker_client.images.remove(f"{project_name}:latest", force=True)
print(f"Image {project_name}:latest removed")
except docker.errors.ImageNotFound:
print(f"Image {project_name}:latest not found (already removed)")
except Exception as image_error:
print(f"Warning: Image removal failed: {image_error}")
# Entferne Projektverzeichnis mit robuster Methode
project_path = os.path.join(PROJECTS_DIR, project_name)
if os.path.exists(project_path):
print(f"Removing project directory: {project_path}")
# Versuche zuerst normale Entfernung
success = self.force_remove_directory(project_path)
if not success:
return False, f"Konnte Projektverzeichnis nicht vollständig entfernen. Versuchen Sie es als Administrator oder entfernen Sie {project_path} manuell."
# Prüfe ob Verzeichnis wirklich entfernt wurde
if os.path.exists(project_path):
return False, f"Projektverzeichnis {project_path} konnte nicht vollständig entfernt werden. Bitte manuell löschen."
print(f"Project {project_name} successfully removed")
return True, f"Projekt {project_name} vollständig entfernt"
except Exception as e:
error_msg = f"Fehler beim Entfernen: {str(e)}"
print(f"Remove project error: {error_msg}")
# Gebe hilfreiche Tipps für Windows-spezifische Probleme
if "Zugriff verweigert" in str(e) or "Access is denied" in str(e):
error_msg += "\n\nTipp: Versuchen Sie:\n1. Alle Git-Clients schließen\n2. Als Administrator ausführen\n3. Antivirus temporär deaktivieren\n4. Manuell löschen: " + os.path.join(PROJECTS_DIR, project_name)
return False, error_msg
# Globale Instanz
project_manager = ProjectManager()
@app.route('/')
def index():
"""Hauptseite mit Projektübersicht"""
config = project_manager.load_config()
# Lade lokale Projekte
projects = []
if os.path.exists(PROJECTS_DIR):
for project_name in os.listdir(PROJECTS_DIR):
project_path = os.path.join(PROJECTS_DIR, project_name)
if os.path.isdir(project_path):
info = project_manager.get_project_info(project_name)
if info:
projects.append(info)
return render_template('index.html', projects=projects, config=config)
@app.route('/refresh_projects')
def refresh_projects():
"""Aktualisiere Projektliste von URL"""
config = project_manager.load_config()
if not config.get('project_list_url'):
flash('Keine Projekt-URL konfiguriert', 'error')
return redirect(url_for('index'))
projects = project_manager.fetch_project_list(config['project_list_url'])
config['projects'] = projects
project_manager.save_config(config)
flash(f'{len(projects)} Projekte gefunden und aktualisiert', 'success')
return redirect(url_for('index'))
@app.route('/available_projects')
def available_projects():
"""Zeige verfügbare Projekte zum Installieren"""
config = project_manager.load_config()
return render_template('available_projects.html', projects=config.get('projects', []))
@app.route('/install_project', methods=['POST'])
def install_project():
"""Installiere Projekt"""
project_url = request.form.get('project_url')
project_name = request.form.get('project_name')
if not project_url or not project_name:
return jsonify({'success': False, 'message': 'URL und Name erforderlich'})
# Klone Projekt
success, message = project_manager.clone_project(project_url, project_name)
if success:
# Versuche automatisch zu bauen
build_success, build_message = project_manager.build_project(project_name)
if build_success:
message += f" und gebaut: {build_message}"
else:
message += f" (Build fehlgeschlagen: {build_message})"
return jsonify({'success': success, 'message': message})
@app.route('/build_project/<project_name>')
def build_project(project_name):
"""Baue Projekt"""
success, message = project_manager.build_project(project_name)
flash(message, 'success' if success else 'error')
return redirect(url_for('index'))
@app.route('/start_project/<project_name>')
def start_project(project_name):
"""Starte Projekt"""
port = request.args.get('port', None)
# Wenn kein Port angegeben, finde einen freien
if not port:
available_port = project_manager.find_available_port()
if available_port:
port = available_port
else:
flash('Kein freier Port verfügbar', 'error')
return redirect(url_for('project_details', project_name=project_name))
success, message = project_manager.start_project(project_name, port)
# Überprüfe ob es ein API-Request ist (AJAX)
if request.headers.get('Accept') == 'application/json':
return jsonify({'success': success, 'message': message, 'port': port})
flash(message, 'success' if success else 'error')
# Umleitung basierend auf Erfolg
if success:
return redirect(url_for('project_details', project_name=project_name))
else:
return redirect(url_for('project_details', project_name=project_name))
@app.route('/stop_project/<project_name>')
def stop_project(project_name):
"""Stoppe Projekt"""
success, message = project_manager.stop_project(project_name)
flash(message, 'success' if success else 'error')
return redirect(url_for('index'))
@app.route('/remove_project/<project_name>')
def remove_project(project_name):
"""Entferne Projekt"""
success, message = project_manager.remove_project(project_name)
flash(message, 'success' if success else 'error')
return redirect(url_for('index'))
@app.route('/config', methods=['GET', 'POST'])
def config():
"""Konfigurationsseite"""
if request.method == 'POST':
config = {
'project_list_url': request.form.get('project_list_url', ''),
'auto_refresh_minutes': int(request.form.get('auto_refresh_minutes', 30)),
'docker_registry': request.form.get('docker_registry', ''),
'projects': project_manager.load_config().get('projects', [])
}
project_manager.save_config(config)
flash('Konfiguration gespeichert', 'success')
return redirect(url_for('config'))
config = project_manager.load_config()
return render_template('config.html', config=config)
@app.route('/project_details/<project_name>')
def project_details(project_name):
"""Projektdetails und Konfiguration"""
info = project_manager.get_project_info(project_name)
if not info:
flash('Projekt nicht gefunden', 'error')
return redirect(url_for('index'))
# Lade .env Datei falls vorhanden
env_content = ''
env_path = os.path.join(info['path'], '.env')
if os.path.exists(env_path):
with open(env_path, 'r', encoding='utf-8') as f:
env_content = f.read()
elif info['has_env_example']:
env_example_path = os.path.join(info['path'], '.env.example')
with open(env_example_path, 'r', encoding='utf-8') as f:
env_content = f.read()
return render_template('project_details.html', project=info, env_content=env_content)
@app.route('/save_env/<project_name>', methods=['POST'])
def save_env(project_name):
"""Speichere .env Konfiguration"""
env_content = request.form.get('env_content', '')
project_path = os.path.join(PROJECTS_DIR, project_name)
env_path = os.path.join(project_path, '.env')
try:
with open(env_path, 'w', encoding='utf-8') as f:
f.write(env_content)
flash('.env Datei gespeichert', 'success')
except Exception as e:
flash(f'Fehler beim Speichern: {str(e)}', 'error')
return redirect(url_for('project_details', project_name=project_name))
@app.route('/api/system_status')
def api_system_status():
"""API Endpoint für Systemstatus"""
status = {
'docker': {'available': False, 'version': None, 'status': 'checking'},
'git': {'available': False, 'version': None, 'status': 'checking'},
'project_dir': {'available': os.path.exists(PROJECTS_DIR), 'status': 'ok'},
'disk_space': {'available': True, 'free': 'Unknown', 'status': 'ok'}
}
# Docker Status
try:
result = subprocess.run(['docker', '--version'], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
version = result.stdout.strip()
# Teste Docker-Daemon
try:
subprocess.run(['docker', 'ps'], capture_output=True, text=True, timeout=5, check=True)
status['docker'] = {'available': True, 'version': version, 'status': 'running'}
except subprocess.CalledProcessError:
status['docker'] = {'available': True, 'version': version, 'status': 'daemon_stopped'}
else:
status['docker'] = {'available': False, 'version': None, 'status': 'not_installed'}
except subprocess.TimeoutExpired:
status['docker'] = {'available': False, 'version': None, 'status': 'timeout'}
except FileNotFoundError:
status['docker'] = {'available': False, 'version': None, 'status': 'not_found'}
except Exception as e:
status['docker'] = {'available': False, 'version': None, 'status': f'error: {str(e)}'}
# Git Status
try:
result = subprocess.run(['git', '--version'], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
status['git'] = {'available': True, 'version': result.stdout.strip(), 'status': 'ok'}
else:
status['git'] = {'available': False, 'version': None, 'status': 'error'}
except FileNotFoundError:
status['git'] = {'available': False, 'version': None, 'status': 'not_found'}
except Exception as e:
status['git'] = {'available': False, 'version': None, 'status': f'error: {str(e)}'}
return jsonify(status)
@app.route('/api/test_connection', methods=['POST'])
def api_test_connection():
"""Test Verbindung zu Projektliste URL"""
data = request.get_json()
url = data.get('url', '')
if not url:
return jsonify({'success': False, 'error': 'Keine URL angegeben'})
try:
projects = project_manager.fetch_project_list(url)
return jsonify({
'success': True,
'projects_found': len(projects),
'projects': projects[:5] # Erste 5 als Beispiel
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/container_logs/<project_name>')
def api_container_logs(project_name):
"""Hole Container-Logs für Debugging"""
try:
if not project_manager.docker_available:
return jsonify({'success': False, 'error': 'Docker nicht verfügbar'})
container = project_manager.docker_client.containers.get(project_name)
logs = container.logs(tail=100).decode('utf-8', errors='ignore')
return jsonify({
'success': True,
'logs': logs,
'status': container.status,
'container_id': container.id
})
except docker.errors.NotFound:
return jsonify({'success': False, 'error': 'Container nicht gefunden'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/container_inspect/<project_name>')
def api_container_inspect(project_name):
"""Detaillierte Container-Informationen für Debugging"""
try:
if not project_manager.docker_available:
return jsonify({'success': False, 'error': 'Docker nicht verfügbar'})
container = project_manager.docker_client.containers.get(project_name)
# Hole relevante Container-Details
info = {
'id': container.id,
'status': container.status,
'image': container.image.tags[0] if container.image.tags else 'unknown',
'ports': container.ports,
'environment': container.attrs.get('Config', {}).get('Env', []),
'restart_policy': container.attrs.get('HostConfig', {}).get('RestartPolicy', {}),
'exit_code': container.attrs.get('State', {}).get('ExitCode'),
'error': container.attrs.get('State', {}).get('Error'),
'started_at': container.attrs.get('State', {}).get('StartedAt'),
'finished_at': container.attrs.get('State', {}).get('FinishedAt')
}
return jsonify({
'success': True,
'container_info': info
})
except docker.errors.NotFound:
return jsonify({'success': False, 'error': 'Container nicht gefunden'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/container_stats/<project_name>')
def api_container_stats(project_name):
"""Container Statistiken abrufen"""
try:
if project_manager.docker_client:
container = project_manager.docker_client.containers.get(project_name)
stats = container.stats(stream=False)
# Vereinfachte Statistiken
cpu_percent = 0
memory_mb = 0
if 'cpu_stats' in stats and 'precpu_stats' in stats:
cpu_delta = stats['cpu_stats']['cpu_usage']['total_usage'] - stats['precpu_stats']['cpu_usage']['total_usage']
system_delta = stats['cpu_stats']['system_cpu_usage'] - stats['precpu_stats']['system_cpu_usage']
if system_delta > 0:
cpu_percent = (cpu_delta / system_delta) * 100.0
if 'memory_stats' in stats:
memory_mb = stats['memory_stats'].get('usage', 0) / 1024 / 1024
return jsonify({
'success': True,
'stats': {
'cpu': f"{cpu_percent:.1f}",
'memory': f"{memory_mb:.0f}",
'network_in': 'N/A',
'network_out': 'N/A'
}
})
else:
return jsonify({'success': False, 'error': 'Docker Client nicht verfügbar'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/restart_project/<project_name>', methods=['POST'])
def api_restart_project(project_name):
"""Projekt Container neustarten"""
try:
if project_manager.docker_client:
container = project_manager.docker_client.containers.get(project_name)
container.restart()
return jsonify({'success': True, 'message': f'Container {project_name} neu gestartet'})
else:
return jsonify({'success': False, 'error': 'Docker Client nicht verfügbar'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/clear_cache', methods=['POST'])
def api_clear_cache():
"""Cache leeren"""
try:
# Hier könnte Cache-Logik implementiert werden
return jsonify({'success': True, 'message': 'Cache geleert'})
except Exception as e:
return jsonify({'success': False, 'message': f'Fehler: {str(e)}'})
@app.route('/api/export_config')
def api_export_config():
"""Konfiguration exportieren"""
try:
config = project_manager.load_config()
response = app.response_class(
response=json.dumps(config, indent=2, ensure_ascii=False),
status=200,
mimetype='application/json',
headers={'Content-Disposition': 'attachment; filename=app_installer_config.json'}
)
return response
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/import_config', methods=['POST'])
def api_import_config():
"""Konfiguration importieren"""
try:
config = request.get_json()
project_manager.save_config(config)
return jsonify({'success': True, 'message': 'Konfiguration importiert'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/reset_config', methods=['POST'])
def api_reset_config():
"""Konfiguration zurücksetzen"""
try:
default_config = {
'project_list_url': '',
'auto_refresh_minutes': 30,
'docker_registry': '',
'projects': []
}
project_manager.save_config(default_config)
return jsonify({'success': True, 'message': 'Konfiguration zurückgesetzt'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/docker_status')
def docker_status():
"""Docker Status und Diagnose Seite"""
return render_template('docker_status.html')
@app.route('/api/docker_diagnose')
def api_docker_diagnose():
"""Docker Diagnose API"""
try:
# Importiere Diagnose-Funktion
from docker_diagnose import diagnose_docker_status
results = {
'timestamp': datetime.now().isoformat(),
'checks': {},
'recommendations': []
}
# Docker Version
try:
result = subprocess.run(['docker', '--version'], capture_output=True, text=True, timeout=10)
if result.returncode == 0:
results['checks']['docker_version'] = {
'success': True,
'output': result.stdout.strip(),
'details': 'Docker ist installiert'
}
else:
results['checks']['docker_version'] = {
'success': False,
'output': result.stderr.strip(),
'details': 'Docker Kommando fehlgeschlagen'
}
results['recommendations'].append('Docker Desktop installieren')
except FileNotFoundError:
results['checks']['docker_version'] = {
'success': False,
'output': 'Docker nicht gefunden',
'details': 'Docker ist nicht installiert'
}
results['recommendations'].append('Docker Desktop von https://docker.com herunterladen')
# Docker Daemon
if project_manager.docker_available:
try:
project_manager.docker_client.ping()
results['checks']['docker_daemon'] = {
'success': True,
'output': 'Docker Daemon läuft',
'details': 'Verbindung erfolgreich'
}
except Exception as e:
results['checks']['docker_daemon'] = {
'success': False,
'output': 'Daemon nicht erreichbar',
'details': str(e)
}
results['recommendations'].append('Docker Desktop starten')
else:
results['checks']['docker_daemon'] = {
'success': False,
'output': 'Daemon nicht verfügbar',
'details': 'Docker Client nicht initialisiert'
}
results['recommendations'].append('Docker Desktop starten')
# Container Status
if project_manager.docker_available:
try:
containers = project_manager.docker_client.containers.list(all=True)
results['checks']['containers'] = {
'success': True,
'output': f'{len(containers)} Container',
'details': f'Gefunden: {len(containers)} Container',
'containers': [{'name': c.name, 'status': c.status} for c in containers]
}
except Exception as e:
results['checks']['containers'] = {
'success': False,
'output': 'Container-Check fehlgeschlagen',
'details': str(e)
}
# Images
if project_manager.docker_available:
try:
images = project_manager.docker_client.images.list()
results['checks']['images'] = {
'success': True,
'output': f'{len(images)} Images',
'details': f'Verfügbare Images: {len(images)}'
}
except Exception as e:
results['checks']['images'] = {
'success': False,
'output': 'Image-Check fehlgeschlagen',
'details': str(e)
}
return jsonify(results)
except Exception as e:
return jsonify({
'error': str(e),
'timestamp': datetime.now().isoformat()
}), 500
@app.route('/api/check_port/<int:port>')
def api_check_port(port):
"""Prüfe ob ein Port verfügbar ist"""
try:
available = project_manager.check_port_availability(port)
return jsonify({
'success': True,
'port': port,
'available': available,
'message': f'Port {port} ist {"verfügbar" if available else "belegt"}'
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
})
@app.route('/api/find_available_port')
def api_find_available_port():
"""Finde einen verfügbaren Port"""
try:
start_port = request.args.get('start', 8080, type=int)
available_port = project_manager.find_available_port(start_port)
if available_port:
return jsonify({
'success': True,
'port': available_port,
'message': f'Port {available_port} ist verfügbar'
})
else:
return jsonify({
'success': False,
'error': f'Kein freier Port ab {start_port} gefunden'
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
})
@app.route('/api/start_project/<project_name>', methods=['POST'])
def api_start_project(project_name):
"""API Endpoint zum Starten eines Projekts"""
try:
data = request.get_json() or {}
port = data.get('port') or request.args.get('port')
# Wenn kein Port angegeben, finde einen freien
if not port:
available_port = project_manager.find_available_port()
if available_port:
port = available_port
else:
return jsonify({
'success': False,
'error': 'Kein freier Port zwischen 8080-8130 verfügbar'
})
success, message = project_manager.start_project(project_name, port)
# Konsistente API-Antwort
response = {
'success': success,
'project_name': project_name
}
if success:
response['message'] = message
response['port'] = port
else:
response['error'] = message
# Bei Port-Konflikten, versuche Alternative zu finden
if 'bereits belegt' in message or 'port is already allocated' in message:
# Extrahiere Port aus Nachricht und finde Alternative
import re
port_match = re.search(r'(\d+)', message)
if port_match:
blocked_port = int(port_match.group(1))
alternative_port = project_manager.find_available_port(blocked_port + 1)
if alternative_port:
response['alternative_port'] = alternative_port
response['error'] = f"Port {blocked_port} ist belegt. Alternativer Port: {alternative_port}"
return jsonify(response)
except Exception as e:
return jsonify({
'success': False,
'error': f'Unerwarteter Fehler: {str(e)}'
})
@app.route('/api/project_status/<project_name>')
def api_project_status(project_name):
"""Hole aktuellen Projektstatus"""
try:
info = project_manager.get_project_info(project_name)
if info:
return jsonify({
'success': True,
'status': info['status'],
'name': info['name'],
'has_dockerfile': info['has_dockerfile'],
'has_env_example': info['has_env_example']
})
else:
return jsonify({'success': False, 'error': 'Projekt nicht gefunden'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/analyze_ports/<project_name>')
def api_analyze_ports(project_name):
"""Analysiere Port-Konfiguration eines Projekts"""
try:
project_path = os.path.join(PROJECTS_DIR, project_name)
result = {
'project_name': project_name,
'dockerfile_ports': [],
'app_file_ports': [],
'image_ports': [],
'recommended_mapping': None,
'analysis': []
}
# Analysiere Dockerfile
if os.path.exists(os.path.join(project_path, 'Dockerfile')):
dockerfile_ports = project_manager.analyze_dockerfile_ports(project_path)
result['dockerfile_ports'] = dockerfile_ports
result['analysis'].append(f"Dockerfile definiert Ports: {', '.join(dockerfile_ports) if dockerfile_ports else 'keine'}")
else:
result['analysis'].append("Kein Dockerfile gefunden")
# Analysiere App-Dateien
app_ports = project_manager.analyze_app_files_for_ports(project_path)
result['app_file_ports'] = app_ports
result['analysis'].append(f"App-Dateien verwenden Ports: {', '.join(app_ports) if app_ports else 'keine gefunden'}")
# Analysiere Image (falls vorhanden)
if project_manager.docker_available:
try:
image_ports = project_manager.detect_container_exposed_ports(project_name)
result['image_ports'] = image_ports
result['analysis'].append(f"Image exponiert Ports: {', '.join(image_ports) if image_ports else 'keine'}")
# Empfohlenes Mapping
if image_ports:
recommended_port = 8080
mapping = project_manager.create_smart_port_mapping(project_name, recommended_port)
result['recommended_mapping'] = mapping
result['analysis'].append(f"Empfohlenes Port-Mapping: {mapping}")
except Exception as e:
result['analysis'].append(f"Image-Analyse fehlgeschlagen: {str(e)}")
else:
result['analysis'].append("Docker nicht verfügbar für Image-Analyse")
return jsonify({
'success': True,
'port_analysis': result
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
})
@app.route('/api/remove_project/<project_name>', methods=['POST'])
def api_remove_project(project_name):
"""API Endpoint zum Entfernen eines Projekts mit detailliertem Feedback"""
try:
success, message = project_manager.remove_project(project_name)
return jsonify({
'success': success,
'message': message,
'project_name': project_name
})
except Exception as e:
return jsonify({
'success': False,
'error': f'Unerwarteter Fehler beim Entfernen: {str(e)}',
'project_name': project_name
})
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)