changes to the deletion process

This commit is contained in:
2026-08-26 14:57:14 +02:00
parent 24b2b31e24
commit 67249cb633
2 changed files with 102 additions and 677 deletions
+37 -227
View File
@@ -458,7 +458,8 @@ def _list_school_instances() -> list:
rows = []
client = None
try:
client, col = _get_collection("school_instances")
# Greife auf "instances" zu (passend zum Async-Provisioning)
client, col = _get_collection("instances")
rows = list(col.find().sort([("updated_at", -1), ("created_at", -1)]))
except PyMongoError:
return []
@@ -472,17 +473,17 @@ def _list_school_instances() -> list:
{
"id": str(row.get("_id") or ""),
"school_name": _sanitize_text(row.get("school_name") or "", 120),
"owner_username": _sanitize_text(row.get("owner_username") or "", 80),
"owner_username": _sanitize_text(row.get("owner_username") or row.get("username") or "", 80),
"subdomain": _sanitize_text(row.get("subdomain") or "", 63),
"domain": _sanitize_text(row.get("domain") or "", 190),
"https_port": int(row.get("https_port") or 0),
"https_port": int(row.get("https_port") or row.get("port") or 0),
"instance_dir": _sanitize_text(row.get("instance_dir") or "", 300),
"app_image_tag": _sanitize_text(row.get("app_image_tag") or "latest", 80),
"library_enabled": bool(row.get("library_enabled", False)),
"status": _sanitize_text(row.get("status") or "Unbekannt", 40),
"nginx_status": _sanitize_text(row.get("nginx_status") or "unbekannt", 80),
"last_message": _sanitize_text(row.get("last_message") or "", 500),
"updated_at": row.get("updated_at") or "",
"updated_at": row.get("updated_at") or row.get("created_at") or "",
}
)
return normalized
@@ -2580,237 +2581,16 @@ def instance_request_delete():
@app.route('/admin/instances', methods=['GET', 'POST'])
@app.route('/admin/instances', methods=['GET'])
@admin_required
def admin_instances():
version_options = _parse_instance_version_options()
available_users = _list_available_instance_users()
if request.method == 'POST':
action = _sanitize_text(request.form.get("action") or "create", 20).lower()
posted_subdomain = _sanitize_text(request.form.get("subdomain") or "", 120)
school_name = _sanitize_text(request.form.get("school_name") or "", 120)
raw_subdomain = _sanitize_text(request.form.get("subdomain") or "", 120)
owner_username = _sanitize_text(request.form.get("owner_username") or "", 80)
app_image_tag = _sanitize_text(request.form.get("app_image_tag") or "latest", 80)
library_enabled = (request.form.get("library_enabled") or "").strip().lower() in {"1", "on", "true", "yes"}
subdomain = _slugify_subdomain(posted_subdomain or raw_subdomain or school_name)
if action == "reload_nginx":
reload_ok, reload_message = _reload_host_nginx()
if not reload_ok:
lowered = (reload_message or "").lower()
if "nicht gefunden" in lowered or "not found" in lowered or "kein nginx-reload-befehl" in lowered:
flash("Host-Nginx kann aus dem Website-Container nicht direkt neu geladen werden.", "error")
flash(_host_reload_hint(), "info")
flash("Nach erfolgreichem Host-Reload bitte 'Reload auf Host bestätigt' ausführen.", "info")
else:
flash(reload_message or "Nginx-Reload fehlgeschlagen.", "error")
return redirect(url_for("admin_instances"))
updated_rows = _promote_manual_nginx_status(
"Nginx erfolgreich neu geladen (Admin-Aktion)."
)
flash("Nginx wurde erfolgreich neu geladen.", "success")
if updated_rows > 0:
flash(f"{updated_rows} Instanz(en) von manual_required auf ok gesetzt.", "info")
else:
flash("Keine Instanz mit nginx_status=manual_required gefunden.", "info")
if reload_message:
flash(reload_message, "info")
return redirect(url_for("admin_instances"))
if action == "confirm_nginx_reload":
updated_rows = _promote_manual_nginx_status(
"Host-Nginx manuell neu geladen und in Admin bestätigt."
)
if updated_rows > 0:
flash(f"{updated_rows} Instanz(en) von manual_required auf ok gesetzt.", "success")
else:
flash("Keine Instanz mit nginx_status=manual_required gefunden.", "info")
return redirect(url_for("admin_instances"))
if action == "delete":
if not _is_valid_subdomain(subdomain):
flash("Ungültige Subdomain für Löschung.", "error")
return redirect(url_for("admin_instances"))
existing_record = _get_school_instance_by_subdomain(subdomain)
target_dir = _instance_dir_path(subdomain)
dir_exists = bool(target_dir and os.path.isdir(target_dir))
if not existing_record and not dir_exists:
flash("Instanz nicht gefunden.", "error")
return redirect(url_for("admin_instances"))
delete_ok, delete_message = _delete_instance_stack(subdomain)
if not delete_ok:
flash(delete_message or "Instanz konnte nicht gelöscht werden.", "error")
return redirect(url_for("admin_instances"))
if existing_record and not _delete_school_instance(subdomain):
flash(
"Instanz wurde technisch gelöscht, aber der Datenbankeintrag konnte nicht entfernt werden.",
"error",
)
return redirect(url_for("admin_instances"))
flash(f"Instanz {subdomain} wurde gelöscht.", "success")
if delete_message:
flash(delete_message, "info")
return redirect(url_for("admin_instances"))
if action == "toggle_library":
target = _get_school_instance_by_subdomain(subdomain)
if not target:
flash("Instanz nicht gefunden.", "error")
return redirect(url_for("admin_instances"))
instance_dir = _resolve_instance_dir(subdomain)
if not instance_dir:
flash("Instanzverzeichnis nicht gefunden.", "error")
return redirect(url_for("admin_instances"))
target_enabled = (request.form.get("library_enabled") or "").strip().lower() in {"1", "on", "true", "yes"}
ok, message = _set_instance_library_enabled(instance_dir, target_enabled)
if not ok:
_upsert_school_instance(
{
"subdomain": subdomain,
"status": "Fehler",
"nginx_status": "error",
"last_message": message,
}
)
flash(message, "error")
return redirect(url_for("admin_instances"))
restart_ok, restart_output = _restart_instance_stack(instance_dir)
client = None
try:
client, col = _get_collection("school_instances")
col.update_one(
{"subdomain": subdomain},
{
"$set": {
"school_name": target.get("school_name") or "",
"owner_username": target.get("owner_username") or "",
"subdomain": subdomain,
"domain": target.get("domain") or f"{subdomain}.{INSTANCE_PARENT_DOMAIN}",
"https_port": int(target.get("https_port") or 0),
"instance_dir": instance_dir,
"app_image_tag": target.get("app_image_tag") or "latest",
"library_enabled": target_enabled,
"status": "Läuft" if restart_ok else "Fehler",
"nginx_status": "ok" if restart_ok else "error",
"last_message": "Bibliothek aktiviert/deaktiviert und Instanz neu gestartet." if restart_ok else _tail_output(restart_output, 18),
"updated_at": _utc_now_iso(),
},
"$setOnInsert": {"created_at": _utc_now_iso()},
},
upsert=True,
)
except PyMongoError:
pass
finally:
if client:
client.close()
if restart_ok:
flash(f"Bibliothek wurde {'aktiviert' if target_enabled else 'deaktiviert'} und die Instanz neu gestartet.", "success")
if restart_output:
flash(_tail_output(restart_output, 12), "info")
else:
flash(f"Bibliothek wurde gespeichert, aber der Neustart ist fehlgeschlagen.\n{_tail_output(restart_output, 18)}", "error")
return redirect(url_for("admin_instances"))
if action not in {"create", "start"}:
flash("Ungültige Aktion für Instanzverwaltung.", "error")
return redirect(url_for("admin_instances"))
if not available_users:
flash("Keine freien Nutzer verfügbar. Bitte zuerst einen neuen Nutzer anlegen oder eine bestehende Instanz löschen.", "error")
return redirect(url_for("admin_instances"))
if not school_name:
flash("Bitte einen Schulnamen angeben.", "error")
return redirect(url_for("admin_instances"))
if not owner_username:
flash("Bitte einen Nutzer zuweisen.", "error")
return redirect(url_for("admin_instances"))
owner_doc = _find_user(owner_username)
if not owner_doc:
flash("Ausgewählter Nutzer wurde nicht gefunden.", "error")
return redirect(url_for("admin_instances"))
if not school_name:
school_name = _sanitize_text(owner_doc.get("display_name") or owner_username, 120)
if app_image_tag not in version_options:
flash("Ungültige Version ausgewählt.", "error")
return redirect(url_for("admin_instances"))
existing_for_owner = _get_instance_for_user(owner_username, "")
if existing_for_owner:
existing_sub = _sanitize_text(existing_for_owner.get("subdomain") or "", 63)
if existing_sub and existing_sub != subdomain:
flash(
f"Nutzer {owner_username} ist bereits der Instanz {existing_sub} zugewiesen. "
"Bitte erst diese Zuweisung ändern.",
"error",
)
return redirect(url_for("admin_instances"))
if not _is_valid_subdomain(subdomain):
flash("Ungültige Subdomain. Erlaubt sind a-z, 0-9 und Bindestriche (3-63 Zeichen).", "error")
return redirect(url_for("admin_instances"))
success, message, details = _run_instance_provision(
action,
school_name,
subdomain,
app_image_tag=app_image_tag,
library_enabled=library_enabled,
)
instance_data = {
"school_name": school_name,
"owner_username": owner_username,
"subdomain": details.get("SUBDOMAIN") or subdomain,
"domain": details.get("DOMAIN") or f"{subdomain}.{INSTANCE_PARENT_DOMAIN}",
"https_port": int((details.get("HTTPS_PORT") or "0") or 0),
"instance_dir": details.get("INSTANCE_DIR") or os.path.join(INSTANCE_BASE_DIR, subdomain),
"app_image_tag": details.get("APP_IMAGE_TAG") or app_image_tag,
"library_enabled": library_enabled if details.get("LIBRARY_ENABLED") is None else details.get("LIBRARY_ENABLED") == "1",
"status": "Läuft" if success else "Fehler",
"nginx_status": details.get("NGINX_STATUS") or ("ok" if success else "error"),
"last_message": message,
}
_upsert_school_instance(instance_data)
if success:
flash(f"Instanz gestartet: {instance_data['domain']}", "success")
if message:
flash(message, "info")
else:
flash(message or "Instanz konnte nicht gestartet werden.", "error")
return redirect(url_for("admin_instances"))
instances = _list_school_instances()
instance_dashboard = _build_instance_dashboard(instances)
return render_template(
"admin_instances.html",
instances=instances,
users=_list_users_for_admin(),
available_users=available_users,
instance_dashboard=instance_dashboard,
version_options=version_options,
instance_repo_url=INSTANCE_REPO_URL,
parent_domain=INSTANCE_PARENT_DOMAIN,
base_dir=INSTANCE_BASE_DIR,
provision_script=INSTANCE_PROVISION_SCRIPT,
)
@@ -3869,6 +3649,36 @@ def tenant_status():
return jsonify({"status": "pending"}), 200
@app.route('/admin/test/drop_all_instances', methods=['POST'])
@login_required
def admin_drop_all_instances():
# 1. Sicherheits-Check: Nur Admins dürfen das!
# Passe diesen Check an deine bestehende Rollenverwaltung an
if not session.get("is_admin") and session.get("role") != "admin":
flash("Zugriff verweigert. Diese Aktion ist nur für Administratoren erlaubt.", "error")
return redirect(url_for('my_instance_management')) # oder zum Dashboard
inst_client, inst_col = _get_collection("instances")
req_client, req_col = _get_collection("instance_requests")
try:
# Löscht alle Dokumente in den beiden Collections
deleted_instances = inst_col.delete_many({}).deleted_count
deleted_requests = req_col.delete_many({}).deleted_count
flash(f"Test-Reset erfolgreich: {deleted_instances} Instanzen und {deleted_requests} Anfragen wurden unwiderruflich gelöscht.", "success")
print(f"[ADMIN ACTION] Alle Instanzen ({deleted_instances}) und Requests ({deleted_requests}) wurden von {session.get('username')} gelöscht.")
except Exception as e:
print(f"[ERROR] Fehler beim Löschen der Datenbank: {e}")
flash("Ein Fehler ist aufgetreten. Datenbank konnte nicht bereinigt werden.", "error")
finally:
inst_client.close()
req_client.close()
# Leite den Admin danach zurück zu einer sicheren Seite
return redirect(url_for('my_instance_management'))
def main():
app.run(host="0.0.0.0", port=4999, debug=False)