changes to the processing
This commit is contained in:
+162
-123
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user