Einstieg Schritt 3: Medium fragt nur WLAN-Weg, Anzahl, Gültigkeit; Installation aus dem Standort
WLAN der Organisation, anderes WLAN oder Kabel; Standort/Zweck nur bei Auswahl; Offline-Medium und Neuinstallation unter Erweitert; Installationsprofil nur für Tuxflotte-Admins (sonst wählt der Server). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bb5229e05e
commit
ecb5c9b63e
4 files changed
+144
-74
No files matched your search
@@ -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;
|
||||
@@ -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.
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -141,75 +141,87 @@
|
||||
</p>
|
||||
{% endif %}
|
||||
<form method="post" action="/installationsmedium">
|
||||
<label for="wifi_ssid">WLAN-SSID</label>
|
||||
<input type="text" id="wifi_ssid" name="wifi_ssid" value="{{ konfiguration.wifi_ssid if konfiguration else '' }}">
|
||||
<fieldset>
|
||||
<legend><strong>Wie kommen die Geräte bei der Installation ins Internet?</strong></legend>
|
||||
{% if org_wlan %}
|
||||
<label>
|
||||
<input type="radio" name="wlan_quelle" value="org" {% if wlan_quelle == "org" %}checked{% endif %}>
|
||||
Über das WLAN der Organisation: <strong>{{ org_wlan.ssid }}</strong>
|
||||
</label>
|
||||
{% endif %}
|
||||
<label>
|
||||
<input type="radio" name="wlan_quelle" value="eigen" {% if wlan_quelle == "eigen" %}checked{% endif %}>
|
||||
Über ein anderes WLAN{% if org_wlan %}, etwa ein Einrichtungs-WLAN{% endif %}
|
||||
</label>
|
||||
<div class="einrueckung">
|
||||
<label for="wifi_ssid">WLAN-Name (SSID)</label>
|
||||
<input type="text" id="wifi_ssid" name="wifi_ssid" value="{{ konfiguration.wifi_ssid if konfiguration and konfiguration.wifi_ssid else '' }}" autocomplete="off" spellcheck="false">
|
||||
<label for="wifi_psk">WLAN-Passwort</label>
|
||||
<input type="password" id="wifi_psk" name="wifi_psk" autocomplete="new-password"
|
||||
placeholder="{{ '•••••••• (gespeichert – zum Ändern neu eingeben)' if konfiguration and konfiguration.hat_wifi_psk else '' }}">
|
||||
<label>
|
||||
<input type="checkbox" name="offenes_wlan"
|
||||
{% if konfiguration and konfiguration.wifi_ssid and not konfiguration.hat_wifi_psk %}checked{% endif %}>
|
||||
Offenes WLAN ohne Passwort
|
||||
</label>
|
||||
<small>Die Geräte merken sich dieses WLAN zusätzlich{% if org_wlan %} zum WLAN der Organisation{% endif %}.</small>
|
||||
</div>
|
||||
<label>
|
||||
<input type="radio" name="wlan_quelle" value="kabel" {% if wlan_quelle == "kabel" %}checked{% endif %}>
|
||||
Über ein Netzwerkkabel
|
||||
</label>
|
||||
{% if not org_wlan %}
|
||||
<small>Tipp: Unter <a href="/organisation#wlan">Organisation → WLAN der Organisation</a> eingetragen, bekommen alle Geräte das WLAN dauerhaft, auch per Kabel installierte.</small>
|
||||
{% endif %}
|
||||
<small>Das WLAN-Passwort steht auf dem Stick. Bewahre ihn sicher auf und lösche ihn nach der Einrichtung.</small>
|
||||
</fieldset>
|
||||
|
||||
<label for="wifi_psk">WLAN-Passwort</label>
|
||||
<input type="password" id="wifi_psk" name="wifi_psk"
|
||||
placeholder="{{ '•••••••• (gespeichert – zum Ändern neu eingeben)' if konfiguration and konfiguration.hat_wifi_psk else '' }}">
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="offenes_wlan"
|
||||
{% if konfiguration and konfiguration.wifi_ssid and not konfiguration.hat_wifi_psk %}checked{% endif %}>
|
||||
Offenes WLAN ohne Passwort
|
||||
</label>
|
||||
<small>
|
||||
Für eine Installation per Netzwerkkabel bleiben beide WLAN-Felder leer.
|
||||
Bei einer anderen SSID ohne neues Passwort wird das gespeicherte Passwort verworfen.
|
||||
</small>
|
||||
|
||||
<p><small>
|
||||
<strong>Hinweis zur Sicherheit:</strong> 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.
|
||||
</small></p>
|
||||
|
||||
<label for="ziel_oe_id">Ziel-OE <small>(wohin die installierten Geräte kommen – ihr Workspace gilt dann)</small></label>
|
||||
{% if organisationseinheiten | length > 1 %}
|
||||
<label for="ziel_oe_id">Standort/Zweck <small>(wohin die installierten Geräte kommen; du kannst sie später auch verschieben)</small></label>
|
||||
<select id="ziel_oe_id" name="ziel_oe_id">
|
||||
{% for oe in organisationseinheiten %}
|
||||
<option value="{{ oe.id }}"
|
||||
{% if konfiguration and konfiguration.ziel_oe_id == oe.id %}selected
|
||||
{% elif not (konfiguration and konfiguration.ziel_oe_id) and oe.ist_standard %}selected{% endif %}>
|
||||
{{ oe.name }}{% if oe.wirksamer_workspace %} – {{ oe.wirksamer_workspace.name }} ({{ oe.wirksamer_workspace.distro }}){% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="bereitstellungsvorlage_id">Installationsprofil <small>(Distro und Plattenaufteilung; muss zur Distro der Ziel-OE passen)</small></label>
|
||||
<select id="bereitstellungsvorlage_id" name="bereitstellungsvorlage_id" required>
|
||||
{% for v in vorlagen %}
|
||||
<option value="{{ v.id }}"
|
||||
{% if konfiguration and konfiguration.bereitstellungsvorlage_id == v.id %}selected
|
||||
{% elif not konfiguration and v.is_default %}selected{% endif %}>
|
||||
{{ v.label }} ({{ v.backend.name }} {{ v.backend.version }})
|
||||
{{ oe.name }}{% if oe.wirksamer_workspace %} – {{ oe.wirksamer_workspace.name }}{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
|
||||
<div class="grid">
|
||||
<label>Kontingent (Anzahl Geräte)
|
||||
<label>Wie viele Geräte?
|
||||
<input type="number" name="max_devices" min="1" value="{{ konfiguration.max_devices if konfiguration else 20 }}" required>
|
||||
</label>
|
||||
<label>Gültigkeit (Tage)
|
||||
<label>Wie lange gültig (Tage)?
|
||||
<input type="number" name="validity_days" min="1" value="{{ konfiguration.validity_days if konfiguration else 30 }}" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<details>
|
||||
<summary>Erweitert</summary>
|
||||
<label>
|
||||
<input type="checkbox" name="offline_medium" {% if konfiguration and konfiguration.offline_medium %}checked{% endif %}>
|
||||
Offline-Medium: Betriebssystem auf dem Stick mitliefern
|
||||
</label>
|
||||
<small>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.</small>
|
||||
<small>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.</small>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="neuinstallation_ohne_rueckfrage" {% if konfiguration and konfiguration.neuinstallation_ohne_rueckfrage %}checked{% endif %}>
|
||||
Bereits installierte Geräte ohne Rückfrage neu installieren
|
||||
</label>
|
||||
<small>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.</small>
|
||||
</fieldset>
|
||||
|
||||
{% if user.is_superuser %}
|
||||
<label for="bereitstellungsvorlage_id">Installationsprofil <small>(nur Tuxflotte; automatisch passt es zum Workspace des Standorts)</small></label>
|
||||
<select id="bereitstellungsvorlage_id" name="bereitstellungsvorlage_id">
|
||||
<option value="">automatisch</option>
|
||||
{% for v in vorlagen %}
|
||||
<option value="{{ v.id }}">{{ v.label }} ({{ v.backend.name }} {{ v.backend.version }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
</details>
|
||||
|
||||
<button type="submit">{{ "Neues Installationsmedium erstellen" if konfiguration else "Installationsmedium erstellen" }}</button>
|
||||
</form>
|
||||
|
||||
Reference in new issue
Block a user