From 1bc96ae2e697aff4134879a7b07eb2f2c4ab3619 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Wed, 26 Aug 2026 12:08:54 +0200 Subject: [PATCH] changes to the processing --- Website/launch.sh | 362 ++++++++++++++++++++++++++++++---------------- Website/main.py | 285 ++++++++++++++++++++---------------- 2 files changed, 397 insertions(+), 250 deletions(-) diff --git a/Website/launch.sh b/Website/launch.sh index 08814dd..44f831a 100755 --- a/Website/launch.sh +++ b/Website/launch.sh @@ -9,171 +9,276 @@ SECRETS_FILE="$SCRIPT_DIR/.mongo-secrets.env" RESET_MONGO_DATA=0 for arg in "$@"; do - case "$arg" in - --reset-mongo-data|-r) - RESET_MONGO_DATA=1 - ;; - esac + case "$arg" in + --reset-mongo-data|-r) + RESET_MONGO_DATA=1 + ;; + esac done ensure_mongo_secrets() { - if [ -f "$SECRETS_FILE" ]; then - # shellcheck disable=SC1090 - source "$SECRETS_FILE" - fi + if [ -f "$SECRETS_FILE" ]; then + # shellcheck disable=SC1090 + source "$SECRETS_FILE" + fi - if [ -z "${MONGO_INITDB_ROOT_PASSWORD:-}" ]; then - MONGO_INITDB_ROOT_PASSWORD="$(openssl rand -hex 24)" - fi - if [ -z "${MONGO_APP_PASSWORD:-}" ]; then - MONGO_APP_PASSWORD="$(openssl rand -hex 24)" - fi + if [ -z "${MONGO_INITDB_ROOT_PASSWORD:-}" ]; then + MONGO_INITDB_ROOT_PASSWORD="$(openssl rand -hex 24)" + fi + if [ -z "${MONGO_APP_PASSWORD:-}" ]; then + MONGO_APP_PASSWORD="$(openssl rand -hex 24)" + fi - cat > "$SECRETS_FILE" < "$SECRETS_FILE" <&2 - return 0 + echo "[WARNUNG] App-User konnte nicht synchronisiert werden; der bestehende Mongo-Volume-Stand verwendet wahrscheinlich andere Root-Credentials. Falls das ein frischer Dev-Stand ist, starte mit MONGO_RESET_DATA=1 neu." >&2 + return 0 } wait_for_mongo_ready() { - local timeout_seconds=180 - local interval=2 - local elapsed=0 - local mongo_container_id="" - local container_state="" - local health_status="" + local timeout_seconds=180 + local interval=2 + local elapsed=0 + local mongo_container_id="" + local container_state="" + local health_status="" - mongo_container_id="$("${DOCKER_CMD[@]}" compose --env-file "$SECRETS_FILE" ps -q mongodb 2>/dev/null || true)" - if [ -z "$mongo_container_id" ]; then - echo "[FEHLER] MongoDB-Container wurde nicht gefunden." >&2 - return 1 - fi + mongo_container_id="$("${DOCKER_CMD[@]}" compose --env-file "$SECRETS_FILE" ps -q mongodb 2>/dev/null || true)" + if [ -z "$mongo_container_id" ]; then + echo "[FEHLER] MongoDB-Container wurde nicht gefunden." >&2 + return 1 + fi - while [ $elapsed -lt $timeout_seconds ]; do - container_state="$(${DOCKER_CMD[@]} inspect -f '{{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}}' "$mongo_container_id" 2>/dev/null || true)" - health_status="${container_state#* }" - if [ "$health_status" = "healthy" ]; then - return 0 - fi - if [ "${container_state%% *}" = "running" ] && [ -z "$health_status" ]; then - return 0 - fi - if [ "${container_state%% *}" = "exited" ] || [ "${container_state%% *}" = "dead" ]; then - break - fi - sleep $interval - elapsed=$((elapsed + interval)) - done + while [ $elapsed -lt $timeout_seconds ]; do + container_state="$(${DOCKER_CMD[@]} inspect -f '{{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}}' "$mongo_container_id" 2>/dev/null || true)" + health_status="${container_state#* }" + if [ "$health_status" = "healthy" ]; then + return 0 + fi + if [ "${container_state%% *}" = "running" ] && [ -z "$health_status" ]; then + return 0 + fi + if [ "${container_state%% *}" = "exited" ] || [ "${container_state%% *}" = "dead" ]; then + break + fi + sleep $interval + elapsed=$((elapsed + interval)) + done - echo "[FEHLER] MongoDB wurde nicht rechtzeitig gestartet. Status: ${container_state:-unbekannt}" >&2 - "${DOCKER_CMD[@]}" compose --env-file "$SECRETS_FILE" ps mongodb >&2 || true - "${DOCKER_CMD[@]}" compose --env-file "$SECRETS_FILE" logs --tail 80 mongodb >&2 || true - return 1 + echo "[FEHLER] MongoDB wurde nicht rechtzeitig gestartet. Status: ${container_state:-unbekannt}" >&2 + "${DOCKER_CMD[@]}" compose --env-file "$SECRETS_FILE" ps mongodb >&2 || true + "${DOCKER_CMD[@]}" compose --env-file "$SECRETS_FILE" logs --tail 80 mongodb >&2 || true + return 1 } resolve_docker_cmd() { - if docker info >/dev/null 2>&1; then - DOCKER_CMD=(docker) - return 0 - fi + if docker info >/dev/null 2>&1; then + DOCKER_CMD=(docker) + return 0 + fi - if sudo -n docker info >/dev/null 2>&1; then - DOCKER_CMD=(sudo docker) - return 0 - fi + if sudo -n docker info >/dev/null 2>&1; then + DOCKER_CMD=(sudo docker) + return 0 + fi - if command -v sudo >/dev/null 2>&1 && [ -t 0 ]; then - echo "Docker-Zugriff ohne Gruppe erkannt. Verwende sudo docker (ggf. Passwortabfrage)." - if sudo docker info >/dev/null 2>&1; then - DOCKER_CMD=(sudo docker) - return 0 - fi - fi + if command -v sudo >/dev/null 2>&1 && [ -t 0 ]; then + echo "Docker-Zugriff ohne Gruppe erkannt. Verwende sudo docker (ggf. Passwortabfrage)." + if sudo docker info >/dev/null 2>&1; then + DOCKER_CMD=(sudo docker) + return 0 + fi + fi - echo "[FEHLER] Kein Zugriff auf den Docker Daemon (/var/run/docker.sock)." >&2 - echo "Führe das Script mit sudo aus oder füge deinen User zur docker-Gruppe hinzu:" >&2 - echo " sudo usermod -aG docker $USER" >&2 - echo "Danach neu einloggen und Script erneut starten." >&2 - exit 3 + echo "[FEHLER] Kein Zugriff auf den Docker Daemon (/var/run/docker.sock)." >&2 + echo "Führe das Script mit sudo aus oder füge deinen User zur docker-Gruppe hinzu:" >&2 + echo " sudo usermod -aG docker $USER" >&2 + echo "Danach neu einloggen und Script erneut starten." >&2 + exit 3 } compose() { - "${DOCKER_CMD[@]}" compose --env-file "$SECRETS_FILE" "$@" + "${DOCKER_CMD[@]}" compose --env-file "$SECRETS_FILE" "$@" } is_truthy() { - case "${1:-}" in - 1|true|TRUE|yes|YES|on|ON) - return 0 - ;; - *) - return 1 - ;; - esac + case "${1:-}" in + 1|true|TRUE|yes|YES|on|ON) + return 0 + ;; + *) + return 1 + ;; + esac } reset_mongo_data_if_requested() { - if ! is_truthy "${MONGO_RESET_DATA:-0}" && [ "$RESET_MONGO_DATA" -ne 1 ]; then - return 0 - fi + if ! is_truthy "${MONGO_RESET_DATA:-0}" && [ "$RESET_MONGO_DATA" -ne 1 ]; then + return 0 + fi - echo "[WARNUNG] Setze den MongoDB-Volume-Stand zurück, damit die aktuellen Secrets neu initialisiert werden." >&2 - "${DOCKER_CMD[@]}" compose --env-file "$SECRETS_FILE" down -v --remove-orphans + echo "[WARNUNG] Setze den MongoDB-Volume-Stand zurück, damit die aktuellen Secrets neu initialisiert werden." >&2 + "${DOCKER_CMD[@]}" compose --env-file "$SECRETS_FILE" down -v --remove-orphans } build_website_image() { - if compose build website; then - return 0 - fi + if compose build website; then + return 0 + fi - echo "[WARNUNG] Standard-Build fehlgeschlagen, versuche Legacy-Build ohne BuildKit erneut..." >&2 - DOCKER_BUILDKIT=0 COMPOSE_DOCKER_CLI_BUILD=0 compose build website + echo "[WARNUNG] Standard-Build fehlgeschlagen, versuche Legacy-Build ohne BuildKit erneut..." >&2 + DOCKER_BUILDKIT=0 COMPOSE_DOCKER_CLI_BUILD=0 compose build website } +# ============================================================================ +# WATCHER LOGIK FÜR ALLE TENANT AKTIONEN (add, remove, restart) +# ============================================================================ +watcher_loop() { + local trigger_dir="$1" + echo "[INFO] Tenant Watcher gestartet. Lausche in $trigger_dir..." + + while true; do + for file in "$trigger_dir"/*.json; do + # Überspringen, wenn keine Dateien da sind + [ -f "$file" ] || continue + + echo "[INFO] Verarbeite Trigger: $file" + + # JSON auslesen (mit jq oder grep als Fallback) + if command -v jq >/dev/null 2>&1; then + ACTION=$(jq -r '.action // "add"' "$file" 2>/dev/null || echo "add") + SLUG=$(jq -r '.slug // empty' "$file" 2>/dev/null || true) + PORT=$(jq -r '.port // empty' "$file" 2>/dev/null || true) + PASS=$(jq -r '.password // empty' "$file" 2>/dev/null || true) + else + # Fallback, falls jq nicht installiert ist + ACTION=$(grep -o '"action": *"[^"]*"' "$file" | cut -d'"' -f4 || echo "add") + [ -z "$ACTION" ] && ACTION="add" + SLUG=$(grep -o '"slug": *"[^"]*"' "$file" | cut -d'"' -f4 || true) + PORT=$(grep -o '"port": *[0-9]*' "$file" | cut -d':' -f2 | tr -d ' ' || true) + PASS=$(grep -o '"password": *"[^"]*"' "$file" | cut -d'"' -f4 || true) + fi + + if [ -z "$SLUG" ]; then + echo "[ERROR] Ungültige Payload in $file (Slug fehlt). Wird gelöscht." >&2 + rm -f "$file" + continue + fi + + case "$ACTION" in + "add") + if [ -n "$PORT" ] && [ -n "$PASS" ]; then + if "$SCRIPT_DIR/manage-tenant.sh" add "$SLUG" "$PORT" "$PASS"; then + echo "[SUCCESS] Tenant bereitgestellt: $SLUG" + else + echo "[ERROR] Fehler beim Bereitstellen von: $SLUG" >&2 + fi + else + echo "[ERROR] Fehlende Port/Passwort-Daten für add in $file" >&2 + fi + ;; + "remove") + if "$SCRIPT_DIR/manage-tenant.sh" remove "$SLUG"; then + echo "[SUCCESS] Tenant gelöscht: $SLUG" + else + echo "[ERROR] Fehler beim Löschen von: $SLUG" >&2 + fi + ;; + "restart") + if "$SCRIPT_DIR/manage-tenant.sh" restart-tenant "$SLUG"; then + echo "[SUCCESS] Tenant neu gestartet: $SLUG" + else + echo "[ERROR] Fehler beim Neustart von: $SLUG" >&2 + fi + ;; + *) + echo "[ERROR] Unbekannte Aktion ($ACTION) in $file" >&2 + ;; + esac + + # Nach Verarbeitung Datei restlos löschen + rm -f "$file" + done + sleep 2 + done +} + +start_tenant_watcher() { + local trigger_dir="$SCRIPT_DIR/triggers" + local pid_file="$SCRIPT_DIR/.watcher.pid" + local log_file="$SCRIPT_DIR/watcher.log" + + # Erstelle Trigger-Verzeichnis + if [ ! -d "$trigger_dir" ]; then + mkdir -p "$trigger_dir" + chmod 777 "$trigger_dir" + fi + + # Beende alten Watcher, falls dieser noch läuft + if [ -f "$pid_file" ]; then + local old_pid + old_pid=$(cat "$pid_file") + if kill -0 "$old_pid" 2>/dev/null; then + echo "Beende alten Watcher-Prozess (PID: $old_pid)..." + kill "$old_pid" || true + fi + fi + + # Starte den Watcher im Hintergrund + echo "Starte asynchronen Tenant-Watcher im Hintergrund..." + watcher_loop "$trigger_dir" >> "$log_file" 2>&1 & + + # Speichere die PID + echo $! > "$pid_file" + echo "Watcher läuft! Logs findest du in: $log_file" +} +# ============================================================================ + + export SESSION_COOKIE_SECURE="0" export INSTANCE_TLS_MODE="development" export INSTANCE_PARENT_DOMAIN="${INSTANCE_PARENT_DOMAIN:-meine-domain}" @@ -189,8 +294,8 @@ ensure_mongo_secrets # Ensure required runtime files exist before building if [ ! -f "$SCRIPT_DIR/gunicorn.conf.py" ]; then - echo "[FEHLER] gunicorn.conf.py fehlt in $SCRIPT_DIR. Bitte prüfen." >&2 - exit 2 + echo "[FEHLER] gunicorn.conf.py fehlt in $SCRIPT_DIR. Bitte prüfen." >&2 + exit 2 fi resolve_docker_cmd @@ -203,7 +308,7 @@ wait_for_mongo_ready ensure_mongo_app_user compose up -d --no-deps website -# Wait for website to become healthy (simple HTTP check) +# Wait for website to become healthy check_url="http://localhost:4999" timeout_seconds=60 interval=2 @@ -211,22 +316,25 @@ elapsed=0 echo "Warte auf Website (${check_url}) bis ${timeout_seconds}s..." while [ $elapsed -lt $timeout_seconds ]; do - if curl -sS --max-time 2 "$check_url" >/dev/null 2>&1; then - echo "Website erreichbar nach ${elapsed}s" - break - fi - sleep $interval - elapsed=$((elapsed + interval)) + if curl -sS --max-time 2 "$check_url" >/dev/null 2>&1; then + echo "Website erreichbar nach ${elapsed}s" + break + fi + sleep $interval + elapsed=$((elapsed + interval)) done if [ $elapsed -ge $timeout_seconds ]; then - echo "[FEHLER] Website nicht erreichbar nach ${timeout_seconds}s. Sammle Diagnosedaten..." - echo "--- docker compose ps ---" - compose ps || true - echo "--- docker logs website (tail 200) ---" - "${DOCKER_CMD[@]}" logs --tail 200 website-website-1 || true - echo "Bitte prüfe Container-Logs und nginx-Konfiguration." + echo "[FEHLER] Website nicht erreichbar nach ${timeout_seconds}s. Sammle Diagnosedaten..." + echo "--- docker compose ps ---" + compose ps || true + echo "--- docker logs website (tail 200) ---" + "${DOCKER_CMD[@]}" logs --tail 200 website-website-1 || true + echo "Bitte prüfe Container-Logs und nginx-Konfiguration." fi echo "Website stack is running on http://localhost:4999" -echo "Provisioning creates self-signed certs per subdomain when needed." \ No newline at end of file +echo "Provisioning creates self-signed certs per subdomain when needed." + +# STARTET DEN WATCHER DIREKT HIER IM SCRIPT +start_tenant_watcher \ No newline at end of file diff --git a/Website/main.py b/Website/main.py index d7252f4..49fdd71 100644 --- a/Website/main.py +++ b/Website/main.py @@ -139,6 +139,8 @@ INSTANCE_PROVISION_SCRIPT = os.environ.get( os.path.abspath(os.path.join(BASE_DIR, "provision_instance.sh")), ) +TRIGGER_DIR = "/app/triggers" + def _env_int(name: str, default: int) -> int: value = os.environ.get(name) @@ -2167,32 +2169,46 @@ def book_option_package(): return redirect(url_for("booking_payment", packages=packages_raw, flow=booking_flow)) def background_booking_provisioning(app_instance, prov_id, subdomain, modules_to_provision, user_id, school_name, user_email): - """Hintergrund-Worker that actually deploys the instance using the correct API""" + """Hintergrund-Worker, der die Tenant-Bereitstellung über das Host-Watcher-Triggersystem auslöst.""" with app_instance.app_context(): req_client, req_col = _get_collection("instance_requests") try: - # 1. Set Status to In Progress + # 1. Status auf "in_progress" setzen req_col.update_one({"_id": ObjectId(prov_id)}, {"$set": {"provision_status": "in_progress"}}) - # 2. Dynamically determine next Port and Password - existing_tenants = Instance.list() - used_ports = [t['port'] for t in existing_tenants if 'port' in t] + # 2. Naechsten Port und Admin-Passwort ermitteln + inst_client_check, inst_col_check = _get_collection("instances") + try: + existing_instances = list(inst_col_check.find({}, {"https_port": 1, "port": 1})) + used_ports = [ + doc.get("https_port") or doc.get("port") + for doc in existing_instances + if doc.get("https_port") or doc.get("port") + ] + finally: + inst_client_check.close() + next_port = max(used_ports) + 1 if used_ports else 10002 admin_password = generate_secure_password() - # 3. Create Tenant Shell - success = Instance.new(subdomain, next_port, admin_password) - if not success: - raise Exception("Tenant shell creation failed (Instance.new returned False)") + # 3. Trigger-Datei fuer den Host-Watcher schreiben (ersetzt Instance.new & Instance.edit) + os.makedirs(TRIGGER_DIR, exist_ok=True) + trigger_file = os.path.join(TRIGGER_DIR, f"{subdomain}.json") - # 4. Enable selected Modules - if "starter" not in modules_to_provision: - Instance.edit(subdomain, "starter") + trigger_payload = { + "request_id": str(prov_id), + "slug": subdomain, + "port": next_port, + "password": admin_password, + "modules": list(modules_to_provision) + } + + with open(trigger_file, "w") as f: + json.dump(trigger_payload, f) - for mod in modules_to_provision: - Instance.edit(subdomain, mod) - - # 5. Register Instance in Database + print(f"[INFO] Trigger-Datei fuer Host-Watcher erstellt: {trigger_file}") + + # 4. Instanz-Eintrag in der Datenbank registrieren domain = f"{subdomain}.invario-software.de" instance_doc = { "owner_id": user_id, @@ -2203,7 +2219,8 @@ def background_booking_provisioning(app_instance, prov_id, subdomain, modules_to "status": "ready", "nginx_status": "active", "admin_username": "admin", - "admin_password": admin_password + "admin_password": admin_password, + "created_at": _utc_now_iso() } inst_client, inst_col = _get_collection("instances") @@ -2212,7 +2229,7 @@ def background_booking_provisioning(app_instance, prov_id, subdomain, modules_to finally: inst_client.close() - # 6. Send Email Notification + # 5. Bestaetigungs-E-Mail versenden try: if user_email: send_accreditation_email( @@ -2224,9 +2241,9 @@ def background_booking_provisioning(app_instance, prov_id, subdomain, modules_to contract_pdf_path=f"/var/contracts/{subdomain}_contract.pdf" ) except Exception as mail_err: - print(f"[WARNING] Email sending failed: {mail_err}") + print(f"[WARNING] E-Mail-Versand fehlgeschlagen: {mail_err}") - # 7. Mark Request as Ready for the Frontend + # 6. Status in instance_requests auf "ready" setzen req_col.update_one( {"_id": ObjectId(prov_id)}, {"$set": { @@ -2239,10 +2256,10 @@ def background_booking_provisioning(app_instance, prov_id, subdomain, modules_to "updated_at": _utc_now_iso() }} ) - print(f"[SUCCESS] Provisioning completed for {subdomain}") + print(f"[SUCCESS] Provisioning-Trigger erfolgreich gesendet fuer {subdomain}") except Exception as e: - print(f"[ERROR] Async Provisioning failed for {subdomain}:") + print(f"[ERROR] Async Provisioning fehlgeschlagen fuer {subdomain}:") traceback.print_exc() req_col.update_one({"_id": ObjectId(prov_id)}, {"$set": {"provision_status": "failed", "last_error": str(e)}}) finally: @@ -2372,22 +2389,23 @@ def booking_payment(): flash(f"Gesprächstermin für {selected_package} wurde an das Team gesendet.", "success") return redirect(url_for("user_chat")) - # FLOW 2: Kostenpflichtige Direkt-Buchung mit Provisioning + # FLOW 2: Kostenpflichtige Direkt-Buchung mit Provisioning via Watcher if booking_flow == "payment": booking_number = f"BOOK-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}" raw_base = form_data.get("school_name") or session.get("username") or booking_number base = _slugify_subdomain(raw_base).lower()[:51] + # Prüfung auf doppelte Subdomains via MongoDB 'instances' Collection + inst_check_client, inst_check_col = _get_collection("instances") try: - existing_tenants = Instance.list() or [] - existing_subdomains = [ - t.get("subdomain") or t.get("tenant_slug") - for t in existing_tenants if isinstance(t, dict) - ] + existing_docs = list(inst_check_col.find({}, {"subdomain": 1})) + existing_subdomains = [d.get("subdomain") for d in existing_docs if d.get("subdomain")] except Exception as e: - print(f"[WARNING] Konnte bestehende Instanzen nicht abfragen: {e}") + print(f"[WARNING] Konnte bestehende Subdomains nicht prüfen: {e}") existing_subdomains = [] + finally: + inst_check_client.close() if base in existing_subdomains: flash("Die gewünschte Subdomain (aus dem Schulnamen) existiert bereits. Bitte wähle einen anderen Schulnamen.", "error") @@ -2424,16 +2442,16 @@ def booking_payment(): modules_to_provision = {pkg for pkg in package_list if pkg in valid_modules} app_obj = current_app._get_current_object() - user_id = session.get("user_id") user_email = session.get("email") or form_data.get("contact_email") + # Startet den asynchronen Thread, der die Trigger-Datei ablegt thread = threading.Thread( target=background_booking_provisioning, args=(app_obj, prov_id, subdomain, modules_to_provision, user_id, form_data["school_name"], user_email) ) thread.start() - print(f"[INFO] Background provisioning gestartet für Subdomain: {subdomain}") + print(f"[INFO] Background Provisioning Trigger gestartet für Subdomain: {subdomain}") flash(f"Buchung für {selected_package} wurde erfasst. Die Instanz wird jetzt eingerichtet.", "success") return redirect(url_for("my_instance_management")) @@ -3691,93 +3709,104 @@ def generate_secure_password(length=14): def _utc_now_iso(): return datetime.now(timezone.utc).isoformat() -def run_tenant_provisioning_async(app, user_id, tenant_slug, school_name, user_email, prov_id): - with app.app_context(): - # Hilfsfunktion zum Aktualisieren des Status - def _update_prov_status(status, extra_fields=None): - req_client, req_col = _get_collection("instance_requests") - try: - update_data = { - "provision_status": status, - "updated_at": _utc_now_iso() - } - if extra_fields: - update_data.update(extra_fields) - - req_col.update_one( - {"_id": ObjectId(prov_id)}, - {"$set": update_data} - ) - finally: - req_client.close() - - # 1. Status auf "in_progress" setzen - _update_prov_status("in_progress") - - # GESAMTEN PROZESS IN TRY-EXCEPT PACKEN - try: - # 2. Port dynamisch ermitteln - existing_tenants = Instance.list() - used_ports = [t['port'] for t in existing_tenants if 'port' in t] - next_port = max(used_ports) + 1 if used_ports else 10002 - - admin_password = generate_secure_password() - - # 3. Skript für Tenant-Erstellung ausführen - success = Instance.new(tenant_slug, next_port, admin_password) - if not success: - _update_prov_status("failed", {"last_error": "Tenant shell creation failed"}) - return - - # Module aktivieren - Instance.edit(tenant_slug, "starter") - - # 4. Instanz in der Datenbank anlegen - domain = f"{tenant_slug}.invario-software.de" - instance_doc = { - "owner_id": user_id, - "school_name": school_name, - "subdomain": tenant_slug, - "domain": domain, - "https_port": next_port, - "status": "ready", - "nginx_status": "active", - "admin_username": "admin", - "admin_password": admin_password - } +def _update_request_status(request_id, status, error_message=None): + """ + Updates the status of a provisioning request in the database. + """ + client, col = _get_collection("instance_requests") + try: + update_fields = { + "provision_status": status, + "updated_at": datetime.utcnow() + } + if error_message: + update_fields["last_error"] = error_message - inst_client, inst_col = _get_collection("instances") - try: - inst_result = inst_col.insert_one(instance_doc) - created_inst_id = str(inst_result.inserted_id) - finally: - inst_client.close() + col.update_one( + {"_id": ObjectId(request_id)}, + {"$set": update_fields} + ) + except Exception as e: + print(f"[ERROR] Failed to update request status: {e}") + finally: + client.close() - # 5. E-Mail versenden (isoliert, damit ein Mail-Fehler nicht die Instanz killt) - try: - send_accreditation_email( - recipient=user_email, - domain=domain, - username="admin", - password=admin_password, - # invoice_pdf_path=f"/var/invoices/{tenant_slug}_invoice.pdf", # Testweise auskommentieren - # contract_pdf_path=f"/var/contracts/{tenant_slug}_contract.pdf" # Testweise auskommentieren - ) - except Exception as mail_error: - print(f"[WARNUNG] Mail konnte nicht gesendet werden: {mail_error}") - _update_prov_status("ready", { - "instance_id": created_inst_id, - "url": f"https://{domain}", - "username": "admin", - "password": admin_password - }) +def _create_instance_record(tenant_slug, port, admin_password): + """ + Creates the final database record for the active instance so the + frontend knows the domain, port, and credentials. + """ + client, col = _get_collection("instances") + try: + # Prevent duplicates if the watcher re-runs + existing = col.find_one({"subdomain": tenant_slug}) + if existing: + return existing["_id"] - except Exception as e: - # Wenn irgendetwas im Block abstürzt, Frontend informieren! - print(f"[ERROR] Provisioning fehlgeschlagen für {tenant_slug}:") - traceback.print_exc() - _update_prov_status("failed", {"last_error": str(e)}) + # Fetch the base domain from env variables, fallback to localhost if missing + parent_domain = os.environ.get("INSTANCE_PARENT_DOMAIN", "localhost") + full_domain = f"{tenant_slug}.{parent_domain}" + + new_instance = { + "subdomain": tenant_slug, + "domain": full_domain, + "port": int(port), + "admin_username": "admin", + "admin_password": admin_password, + "created_at": datetime.utcnow(), + "status": "active" + } + result = col.insert_one(new_instance) + return result.inserted_id + except Exception as e: + print(f"[ERROR] Failed to create instance record: {e}") + finally: + client.close() + +def run_tenant_provisioning_async(tenant_slug, next_port, admin_password, user_email, request_id): + """ + Background thread running inside the Flask container. + Drops a trigger payload for the host watcher service. + """ + try: + # 1. Update DB to provisioning state + _update_request_status(request_id, "provisioning") + + # 2. Write trigger file for the host watcher + os.makedirs(TRIGGER_DIR, exist_ok=True) + trigger_file = os.path.join(TRIGGER_DIR, f"{tenant_slug}.json") + + payload = { + "request_id": str(request_id), + "slug": tenant_slug, + "port": next_port, + "password": admin_password + } + + with open(trigger_file, "w") as f: + json.dump(payload, f) + + # 3. Register instance record in MongoDB + _create_instance_record(tenant_slug, next_port, admin_password) + + # 4. Optional: Send accreditation email safely + try: + send_accreditation_email( + recipient=user_email, + domain=f"{tenant_slug}.yourdomain.com", + username="admin", + password=admin_password + ) + except Exception as mail_err: + print(f"[WARN] Mail delivery failed (non-fatal): {mail_err}") + + # Mark request as ready + _update_request_status(request_id, "ready") + + except Exception as e: + print(f"[ERROR] Tenant provisioning failed: {e}") + _update_request_status(request_id, "failed", error_message=str(e)) @app.route('/booking_payment_async', methods=['POST']) @login_required @@ -3823,8 +3852,11 @@ def booking_payment_async(): # WICHTIG: Fängt jegliche Python-Fehler ab und sendet sauberes JSON statt HTML-Crash return jsonify({"success": False, "error": str(e)}), 500 -@app.route('/instance_request/status') +@app.route('/instance_request/status', methods=['GET']) def tenant_status(): + """ + Status endpoint checked by the frontend polling mechanism. + """ slug = request.args.get('slug') req_id = request.args.get('request_id') @@ -3842,12 +3874,19 @@ def tenant_status(): finally: req_client.close() - current_status = prov_req.get("provision_status") if prov_req else None - + if not prov_req: + return jsonify({"status": "pending"}), 200 + + current_status = prov_req.get("provision_status") + + # Fail immediately if background execution crashed if current_status == "failed": - return jsonify({"status": "failed", "error": prov_req.get("last_error", "Unbekannter Fehler")}), 200 - - if not prov_req or current_status not in ["ready", "completed"]: + return jsonify({ + "status": "failed", + "error": prov_req.get("last_error", "Tenant provisioning failed.") + }), 200 + + if current_status not in ["ready", "completed"]: return jsonify({"status": "pending"}), 200 inst_client, inst_col = _get_collection("instances") @@ -3859,10 +3898,11 @@ def tenant_status(): if not instance: return jsonify({"status": "pending"}), 200 + # Perform health check on newly provisioned container endpoint target_url = f"https://{instance['domain']}/health" try: - response = requests.get(target_url, timeout=3) - if response.status_code == 200: + res = requests.get(target_url, timeout=3, verify=False) + if res.status_code == 200: return jsonify({ "status": "ready", "url": f"https://{instance['domain']}", @@ -3873,7 +3913,6 @@ def tenant_status(): pass return jsonify({"status": "pending"}), 200 - def main(): app.run(host="0.0.0.0", port=4999, debug=False)