diff --git a/migrations/0011_installationsmedium_wlan_quelle.sql b/migrations/0011_installationsmedium_wlan_quelle.sql new file mode 100644 index 0000000..998d92f --- /dev/null +++ b/migrations/0011_installationsmedium_wlan_quelle.sql @@ -0,0 +1,15 @@ +BEGIN; + +-- Einstieg vereinfachen, Schritt 3 (platform-docs roadmap/einstieg- +-- vereinfachen.md, 25.10.2026): Woher das Medium sein WLAN nimmt. "org" = +-- WLAN der Organisation (provisioning-server), "eigen" = die Felder +-- wifi_ssid/wifi_psk_encrypted (z. B. ein Einrichtungs-WLAN), "kabel" = +-- keins. + +ALTER TABLE installationsmedium_konfiguration + ADD COLUMN wlan_quelle TEXT NOT NULL DEFAULT 'eigen' + CHECK (wlan_quelle IN ('org', 'eigen', 'kabel')); + +UPDATE installationsmedium_konfiguration SET wlan_quelle = 'kabel' WHERE coalesce(wifi_ssid, '') = ''; + +COMMIT; diff --git a/routers/installationsmedium.py b/routers/installationsmedium.py index 9f6b1f3..4b76cc0 100644 --- a/routers/installationsmedium.py +++ b/routers/installationsmedium.py @@ -35,7 +35,7 @@ def fetch_konfiguration(organization_id: str) -> dict | None: """ SELECT wifi_ssid, wifi_psk_encrypted, bereitstellungsvorlage_id, max_devices, validity_days, enrollment_session_id, iso_build_id, - neuinstallation_ohne_rueckfrage, offline_medium, ziel_oe_id + neuinstallation_ohne_rueckfrage, offline_medium, ziel_oe_id, wlan_quelle FROM installationsmedium_konfiguration WHERE organization_id = %s """, @@ -48,7 +48,7 @@ def fetch_konfiguration(organization_id: str) -> dict | None: (wifi_ssid, wifi_psk_encrypted, bereitstellungsvorlage_id, max_devices, validity_days, enrollment_session_id, iso_build_id, - neuinstallation_ohne_rueckfrage, offline_medium, ziel_oe_id) = row + neuinstallation_ohne_rueckfrage, offline_medium, ziel_oe_id, wlan_quelle) = row # psycopg liefert TEXT-Spalten hier teils als bytes zurueck, nicht str - # dasselbe bekannte, nicht deterministische Verhalten wie in @@ -68,6 +68,7 @@ def fetch_konfiguration(organization_id: str) -> dict | None: "neuinstallation_ohne_rueckfrage": neuinstallation_ohne_rueckfrage, "offline_medium": offline_medium, "ziel_oe_id": str(ziel_oe_id) if ziel_oe_id else None, + "wlan_quelle": wlan_quelle, } @@ -127,16 +128,17 @@ def speichere_konfiguration_basis( def speichere_medium_optionen( organization_id: str, neuinstallation_ohne_rueckfrage: bool, offline_medium: bool, ziel_oe_id: str | None = None, + wlan_quelle: str = "eigen", ) -> None: with get_database_connection() as conn: with conn.cursor() as cur: cur.execute( """ UPDATE installationsmedium_konfiguration - SET neuinstallation_ohne_rueckfrage = %s, offline_medium = %s, ziel_oe_id = %s + SET neuinstallation_ohne_rueckfrage = %s, offline_medium = %s, ziel_oe_id = %s, wlan_quelle = %s WHERE organization_id = %s """, - (neuinstallation_ohne_rueckfrage, offline_medium, ziel_oe_id, organization_id), + (neuinstallation_ohne_rueckfrage, offline_medium, ziel_oe_id, wlan_quelle, organization_id), ) @@ -277,7 +279,21 @@ def _installationsmedium_kontext(user) -> dict: oes_result = anode_request("GET", f"/api/v1/organizations/{organization_id}/organisationseinheiten") organisationseinheiten = oes_result.get("organisationseinheiten", []) if oes_result.get("success") else [] + wlan_result = anode_request("GET", f"/api/v1/organizations/{organization_id}/org-wlan") + org_wlan = wlan_result.get("org_wlan") if wlan_result.get("success") else None + + # Vorauswahl der WLAN-Quelle: wie beim letzten Medium, sonst das WLAN der + # Organisation, sonst Kabel. + if konfiguration: + wlan_quelle = konfiguration["wlan_quelle"] + if wlan_quelle == "org" and not org_wlan: + wlan_quelle = "eigen" if konfiguration["wifi_ssid"] else "kabel" + else: + wlan_quelle = "org" if org_wlan else "kabel" + return { + "org_wlan": org_wlan, + "wlan_quelle": wlan_quelle, "user": user, "konfiguration": konfiguration, "vorlagen": vorlagen, @@ -316,41 +332,64 @@ def installationsmedium_erstellen_ansicht(request: Request, user=Depends(get_cur ) +def _erstellen_fehler(meldung: str): + return RedirectResponse("/installationsmedium/erstellen?" + urlencode({"fehler": meldung}), status_code=303) + + @router.post("/installationsmedium") def installationsmedium_erstellen( + wlan_quelle: str = Form(default=""), wifi_ssid: str = Form(default=""), wifi_psk: str = Form(default=""), - bereitstellungsvorlage_id: str = Form(...), + offenes_wlan: str = Form(default=""), + bereitstellungsvorlage_id: str = Form(default=""), max_devices: int = Form(...), validity_days: int = Form(...), neuinstallation_ohne_rueckfrage: str = Form(default=""), offline_medium: str = Form(default=""), ziel_oe_id: str = Form(default=""), - offenes_wlan: str = Form(default=""), user=Depends(get_current_user), ): + """ + Einstieg vereinfachen, Schritt 3 (25.10.2026): Der Kunde wählt nur noch, + über welches WLAN installiert wird, wie viele Geräte und wie lange. Die + Installation (Distro, Plattenaufteilung) wählt der Server passend zur + Ziel-OE; ein Profil von Hand gibt es nur noch für Tuxflotte-Admins. + """ + organization_id = str(user.organization_id) ohne_rueckfrage = neuinstallation_ohne_rueckfrage == "on" offline = offline_medium == "on" wifi_ssid = wifi_ssid.strip() - if offenes_wlan == "on" or not wifi_ssid: - wifi_psk = "" + if wlan_quelle not in ("org", "eigen", "kabel"): + # Formular vor Schritt 3: WLAN-Felder leer heißt Kabel. + wlan_quelle = "eigen" if wifi_ssid else "kabel" + if not user.is_superuser: + bereitstellungsvorlage_id = "" bisher = fetch_konfiguration(organization_id) - psk_entfernen = psk_verwerfen( - offenes_wlan == "on", wifi_ssid, wifi_psk, bisher["wifi_ssid"] if bisher else None - ) - - speichere_konfiguration_basis( - organization_id, wifi_ssid, wifi_psk, bereitstellungsvorlage_id, max_devices, validity_days, - psk_entfernen=psk_entfernen, - ) - speichere_medium_optionen(organization_id, ohne_rueckfrage, offline, ziel_oe_id or None) - - # Nur fuer den ISO-Bau gebraucht, nie ueber diesen Aufruf hinaus - # gespeichert (siehe Phase-4-Design von provisioning-server) - bei - # leerem Feld wird das zuvor gespeicherte PSK weiterverwendet. - psk_fuer_build = wifi_psk or ("" if psk_entfernen else (lade_gespeichertes_psk(organization_id) or "")) + psk_entfernen = False + if wlan_quelle == "org": + wlan = anode_request("GET", f"/api/v1/organizations/{organization_id}/org-wlan", params={"mit_passwort": "true"}) + org_wlan = wlan.get("org_wlan") if wlan.get("success") else None + if not org_wlan: + return _erstellen_fehler("Für die Organisation ist kein WLAN eingetragen (Organisation → WLAN der Organisation).") + build_ssid, psk_fuer_build = org_wlan["ssid"], org_wlan.get("passwort") or "" + # Die Felder "anderes WLAN" bleiben, wie sie waren. + wifi_ssid, wifi_psk = (bisher["wifi_ssid"] or "") if bisher else "", "" + elif wlan_quelle == "eigen": + if not wifi_ssid: + return _erstellen_fehler("Bitte den Namen des WLANs eingeben oder „Netzwerkkabel“ wählen.") + if offenes_wlan == "on": + wifi_psk = "" + psk_entfernen = psk_verwerfen(offenes_wlan == "on", wifi_ssid, wifi_psk, bisher["wifi_ssid"] if bisher else None) + build_ssid = wifi_ssid + # Nur für den ISO-Bau, nie über diesen Aufruf hinaus gespeichert - + # bei leerem Feld gilt das zuvor gespeicherte PSK. + psk_fuer_build = wifi_psk or ("" if psk_entfernen else (lade_gespeichertes_psk(organization_id) or "")) + else: + build_ssid = psk_fuer_build = "" + wifi_ssid, wifi_psk = (bisher["wifi_ssid"] or "") if bisher else "", "" expires_at = (datetime.now(timezone.utc) + timedelta(days=validity_days)).isoformat() @@ -358,7 +397,7 @@ def installationsmedium_erstellen( "POST", f"/api/v1/organizations/{organization_id}/enrollment-sessions", json={ - "bereitstellungsvorlage_id": bereitstellungsvorlage_id, + "bereitstellungsvorlage_id": bereitstellungsvorlage_id or None, "max_devices": max_devices, "expires_at": expires_at, "neuinstallation_ohne_rueckfrage": ohne_rueckfrage, @@ -366,24 +405,26 @@ def installationsmedium_erstellen( }, ) - # Ziel-OE und Profil passen nicht zusammen (andere Distro) - verständlich - # zurückmelden statt Fehlerseite (Workspace-Creator neu, Schritt F). - if not session_result.get("success") and session_result.get("error") == "ziel_oe_konflikt": - return RedirectResponse( - "/installationsmedium/erstellen?" + urlencode({"fehler": session_result["message"]}), status_code=303 - ) - + # Ziel-OE passt nicht zur Installation oder es gibt keine passende - + # verständlich zurückmelden statt Fehlerseite. if not session_result.get("success"): - raise HTTPException(status_code=400, detail=session_result.get("message", "Enrollment Session konnte nicht erstellt werden.")) + return _erstellen_fehler(session_result.get("message", "Das Medium konnte nicht angelegt werden.")) session_id = session_result["enrollment_session"]["id"] + vorlage_id = session_result["enrollment_session"].get("bereitstellungsvorlage_id") or bereitstellungsvorlage_id + + speichere_konfiguration_basis( + organization_id, wifi_ssid, wifi_psk, vorlage_id, max_devices, validity_days, + psk_entfernen=psk_entfernen, + ) + speichere_medium_optionen(organization_id, ohne_rueckfrage, offline, ziel_oe_id or None, wlan_quelle) build_result = anode_request( "POST", f"/api/v1/organizations/{organization_id}/iso-builds", json={ "activation_code": session_id, - "wifi_ssid": wifi_ssid, + "wifi_ssid": build_ssid, "wifi_psk": psk_fuer_build, # Diagnosezugang (02.10.2026): nur der Hash, und nur wenn der # Root-Admin ihn fuer diese Organisation eingeschaltet hat. diff --git a/templates/base.html b/templates/base.html index 5c95d98..fd49201 100644 --- a/templates/base.html +++ b/templates/base.html @@ -126,6 +126,8 @@ form.inline { display: inline; margin: 0; } form.inline button { width: auto; } button.klein, [role=button].klein { padding: 0.2rem 0.6rem; font-size: 0.85rem; width: auto; margin: 0; } + /* Felder, die zu einer Auswahl darüber gehören (Installationsmedium: anderes WLAN). */ + .einrueckung { margin-left: 1.75rem; } /* Merkmal-Seiten (15.10.2026, Nutzer-Wunsch): Aktiv/Inaktiv/ Zurücksetzen in den Kategorie-Tabellen so kompakt wie .klein. */ details.merkmal-kategorie button { padding: 0.2rem 0.6rem; font-size: 0.85rem; width: auto; margin: 0 0.25rem 0.25rem 0; } diff --git a/templates/installationsmedium.html b/templates/installationsmedium.html index 9ea74f3..93a6768 100644 --- a/templates/installationsmedium.html +++ b/templates/installationsmedium.html @@ -141,75 +141,87 @@

{% endif %}
- - +
+ Wie kommen die Geräte bei der Installation ins Internet? + {% if org_wlan %} + + {% endif %} + +
+ + + + + + Die Geräte merken sich dieses WLAN zusätzlich{% if org_wlan %} zum WLAN der Organisation{% endif %}. +
+ + {% if not org_wlan %} + Tipp: Unter Organisation → WLAN der Organisation eingetragen, bekommen alle Geräte das WLAN dauerhaft, auch per Kabel installierte. + {% endif %} + Das WLAN-Passwort steht auf dem Stick. Bewahre ihn sicher auf und lösche ihn nach der Einrichtung. +
- - - - - - Für eine Installation per Netzwerkkabel bleiben beide WLAN-Felder leer. - Bei einer anderen SSID ohne neues Passwort wird das gespeicherte Passwort verworfen. - - -

- Hinweis zur Sicherheit: SSID und Passwort werden auf dem Installationsmedium - gespeichert. Wer den Stick in die Hand bekommt, kann das Passwort auslesen. Bewahre den - Stick sicher auf und lösche ihn nach der Einrichtung. Wo möglich, installiere per - Netzwerkkabel oder über ein eigenes Einrichtungs-WLAN. Die installierten Geräte verbinden sich - dauerhaft mit dem hier eingetragenen WLAN; dort ist das Passwort nur für Administratoren lesbar. -

- - + {% if organisationseinheiten | length > 1 %} + - - - + {% endif %}
-
-
+
+ Erweitert - Bei der Installation wird nichts heruntergeladen (Netz wird trotzdem für die Anmeldung beim Server gebraucht). Das Medium wird deutlich größer (ca. 3–4,5 GB). Auf Geräten mit wenig Arbeitsspeicher muss der Stick bis zum Ende der Installation stecken bleiben. + Sinnvoll bei langsamem Internet, etwa für einen ganzen Klassensatz: Sonst lädt jedes Gerät bei der Installation deutlich über 1 GB. Netz braucht es trotzdem für die Anmeldung beim Server. Das Medium wird deutlich größer (ca. 3–4,5 GB). Auf Geräten mit wenig Arbeitsspeicher muss der Stick bis zum Ende der Installation stecken bleiben. Sonst fragt die Installation auf einem schon eingerichteten Gerät nach, bevor die Festplatte gelöscht wird. Ohne Rückfrage geeignet, um einen ganzen Raum bewusst neu aufzusetzen – alle Daten auf diesen Geräten gehen verloren. -
+ + {% if user.is_superuser %} + + + {% endif %} +