Boot-Medium neutral benannt (TUXFLOTTE_BOOT_MEDIUM_PATH), Offline-Medium, Neuinstallation ohne Rueckfrage pro Enrollment Session
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
43d0fce764
commit
5ed6d4d478
3 files changed
+93
-22
No files matched your search
@@ -46,15 +46,22 @@ STANDARD_OE_NAME = "Neue Geräte"
|
||||
|
||||
# Phase 4 des Self-Service-ISO-Plans (siehe tuxflotte-installer/scripts/
|
||||
# build_customer_iso.sh). TUXFLOTTE_INSTALLER_DIR ist ein separat auf anode
|
||||
# geklontes Repo (nicht Teil von provisioning-server selbst), MINT_ISO_PATH
|
||||
# das einmalig von der offiziellen Downloadseite geholte Quellabbild.
|
||||
# geklontes Repo (nicht Teil von provisioning-server selbst), BOOT_MEDIUM_PATH
|
||||
# das eigene, distributionsunabhaengige Boot-Medium (build_boot_medium.sh +
|
||||
# publish_boot_medium.sh), aus dem JEDE Kunden-ISO gebaut wird - egal ob Mint,
|
||||
# Debian oder Ubuntu. Hiess frueher MINT_ISO_PATH (aus der Zeit, als die
|
||||
# Mint-Live-ISO gepatcht wurde); die alte Variable gilt als Rueckfallebene.
|
||||
TUXFLOTTE_INSTALLER_DIR = os.environ.get("TUXFLOTTE_INSTALLER_DIR", "/root/tuxflotte-installer")
|
||||
# Absolut aufloesen: run_iso_build() startet build_customer_iso.sh mit
|
||||
# cwd=TUXFLOTTE_INSTALLER_DIR, ein relativer Pfad wuerde also dort statt
|
||||
# relativ zum eigenen Arbeitsverzeichnis (WorkingDirectory des Service)
|
||||
# gesucht - genau das hat den ersten Live-Test auf anode fehlschlagen lassen.
|
||||
MINT_ISO_PATH = str(Path(os.environ.get("TUXFLOTTE_MINT_ISO_PATH", "data/upstream/linuxmint-22.3-cinnamon-64bit.iso")).resolve())
|
||||
# Aus demselben Grund wie MINT_ISO_PATH absolut aufloesen: der als $2 an
|
||||
BOOT_MEDIUM_PATH = str(Path(
|
||||
os.environ.get("TUXFLOTTE_BOOT_MEDIUM_PATH")
|
||||
or os.environ.get("TUXFLOTTE_MINT_ISO_PATH")
|
||||
or "data/upstream/tuxflotte-boot-medium.iso"
|
||||
).resolve())
|
||||
# Aus demselben Grund wie BOOT_MEDIUM_PATH absolut aufloesen: der als $2 an
|
||||
# build_customer_iso.sh uebergebene Ausgabepfad wird von xorriso relativ
|
||||
# zum Subprocess-cwd (TUXFLOTTE_INSTALLER_DIR) interpretiert, nicht relativ
|
||||
# zum eigenen Arbeitsverzeichnis.
|
||||
@@ -275,6 +282,7 @@ class CreateEnrollmentSessionRequest(BaseModel):
|
||||
bereitstellungsvorlage_id: str
|
||||
max_devices: int
|
||||
expires_at: str
|
||||
neuinstallation_ohne_rueckfrage: bool = False
|
||||
|
||||
|
||||
class RechargeEnrollmentSessionRequest(BaseModel):
|
||||
@@ -289,6 +297,8 @@ class CreateIsoBuildRequest(BaseModel):
|
||||
# Diagnosezugang (02.10.2026): nur der SHA-512-crypt-Hash, nie das
|
||||
# Passwort selbst - das kennt allein die Kundenplattform (Root-Admin).
|
||||
root_password_hash: str | None = None
|
||||
# Offline-Medium (Roadmap V5): Golden Image auf die ISO legen.
|
||||
offline_medium: bool = False
|
||||
|
||||
|
||||
class CreateOrganisationseinheitRequest(BaseModel):
|
||||
@@ -609,7 +619,8 @@ def find_enrollment_session(code: str):
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT es.id, es.organization_id, o.name, es.bereitstellungsvorlage_id,
|
||||
es.max_devices, es.devices_used, es.expires_at, es.revoked_at
|
||||
es.max_devices, es.devices_used, es.expires_at, es.revoked_at,
|
||||
es.neuinstallation_ohne_rueckfrage
|
||||
FROM enrollment_sessions es
|
||||
JOIN organizations o ON o.id = es.organization_id
|
||||
WHERE es.id = %s
|
||||
@@ -623,7 +634,7 @@ def find_enrollment_session(code: str):
|
||||
|
||||
(
|
||||
session_id, organization_id, organization_name, bereitstellungsvorlage_id,
|
||||
max_devices, devices_used, expires_at, revoked_at,
|
||||
max_devices, devices_used, expires_at, revoked_at, neuinstallation_ohne_rueckfrage,
|
||||
) = row
|
||||
|
||||
if revoked_at is not None:
|
||||
@@ -638,6 +649,7 @@ def find_enrollment_session(code: str):
|
||||
"organization_id": str(organization_id),
|
||||
"organization_name": organization_name,
|
||||
"bereitstellungsvorlage_id": str(bereitstellungsvorlage_id),
|
||||
"neuinstallation_ohne_rueckfrage": bool(neuinstallation_ohne_rueckfrage),
|
||||
}
|
||||
|
||||
def consume_enrollment_session(session_id: str) -> None:
|
||||
@@ -3481,7 +3493,10 @@ def fetch_enrollment_sessions(organization_id: str):
|
||||
) in cur.fetchall()
|
||||
]
|
||||
|
||||
def create_enrollment_session(organization_id: str, bereitstellungsvorlage_id: str, max_devices: int, expires_at: str):
|
||||
def create_enrollment_session(
|
||||
organization_id: str, bereitstellungsvorlage_id: str, max_devices: int, expires_at: str,
|
||||
neuinstallation_ohne_rueckfrage: bool = False,
|
||||
):
|
||||
session_id = uuid4()
|
||||
|
||||
with get_database_connection() as conn:
|
||||
@@ -3489,10 +3504,12 @@ def create_enrollment_session(organization_id: str, bereitstellungsvorlage_id: s
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO enrollment_sessions
|
||||
(id, organization_id, bereitstellungsvorlage_id, max_devices, expires_at)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
(id, organization_id, bereitstellungsvorlage_id, max_devices, expires_at,
|
||||
neuinstallation_ohne_rueckfrage)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(session_id, organization_id, bereitstellungsvorlage_id, max_devices, expires_at),
|
||||
(session_id, organization_id, bereitstellungsvorlage_id, max_devices, expires_at,
|
||||
neuinstallation_ohne_rueckfrage),
|
||||
)
|
||||
|
||||
return {"id": str(session_id)}
|
||||
@@ -3748,10 +3765,11 @@ def compute_file_sha256(path) -> str:
|
||||
|
||||
return hasher.hexdigest()
|
||||
|
||||
def fetch_vorlage_label_for_session(session_id: str) -> str | None:
|
||||
def fetch_vorlage_for_session(session_id: str) -> tuple[str | None, str | None]:
|
||||
"""
|
||||
Name der Bereitstellungsvorlage einer Enrollment Session - fuer den
|
||||
Boot-Menue-Eintrag der Kunden-ISO (build_customer_iso.sh, VORLAGE_LABEL).
|
||||
(Name der Bereitstellungsvorlage, Backend-Key) einer Enrollment Session -
|
||||
fuer den Boot-Menue-Eintrag (VORLAGE_LABEL) und das Offline-Medium
|
||||
(passendes Golden Image) in build_customer_iso.sh.
|
||||
"""
|
||||
|
||||
try:
|
||||
@@ -3759,19 +3777,40 @@ def fetch_vorlage_label_for_session(session_id: str) -> str | None:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT bv.label
|
||||
SELECT bv.label, b.key
|
||||
FROM enrollment_sessions es
|
||||
JOIN bereitstellungsvorlagen bv ON bv.id = es.bereitstellungsvorlage_id
|
||||
JOIN backends b ON b.id = bv.backend_id
|
||||
WHERE es.id = %s
|
||||
""",
|
||||
(session_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
except Exception:
|
||||
# Rein kosmetisch - ein Fehler hier darf den ISO-Bau nie verhindern.
|
||||
return None
|
||||
# Rein unterstuetzend - ein Fehler hier darf den ISO-Bau nie verhindern.
|
||||
return None, None
|
||||
|
||||
return row[0] if row else None
|
||||
return (row[0], row[1]) if row else (None, None)
|
||||
|
||||
|
||||
def golden_image_path_for_backend(backend_key: str | None) -> Path | None:
|
||||
"""
|
||||
Pfad des Golden Images eines Image-Backends. Der Dateiname steht nur an
|
||||
einer Stelle - GOLDEN_IMAGE_FILENAME im jeweiligen backend.sh des
|
||||
Installers -, deshalb wird er dort gelesen statt hier dupliziert.
|
||||
"""
|
||||
|
||||
if not backend_key or not re.fullmatch(r"[a-z0-9-]+", backend_key):
|
||||
return None
|
||||
backend_file = Path(TUXFLOTTE_INSTALLER_DIR) / "backends" / backend_key / "backend.sh"
|
||||
try:
|
||||
match = re.search(r'^GOLDEN_IMAGE_FILENAME="([^"/]+)"', backend_file.read_text(), re.MULTILINE)
|
||||
except OSError:
|
||||
return None
|
||||
if not match:
|
||||
return None
|
||||
path = GOLDEN_IMAGES_DIR / match.group(1)
|
||||
return path if path.is_file() else None
|
||||
|
||||
|
||||
ROOT_PASSWORD_HASH_PATTERN = re.compile(r"^\$6\$[./A-Za-z0-9]{1,16}\$[./A-Za-z0-9]{86}$")
|
||||
@@ -3780,6 +3819,7 @@ ROOT_PASSWORD_HASH_PATTERN = re.compile(r"^\$6\$[./A-Za-z0-9]{1,16}\$[./A-Za-z0-
|
||||
def run_iso_build(
|
||||
build_id, organization_id: str, activation_code: str, wifi_ssid: str | None, wifi_psk: str | None,
|
||||
root_password_hash: str | None = None,
|
||||
offline_medium: bool = False,
|
||||
) -> None:
|
||||
output_path = ISO_BUILD_OUTPUT_DIR / f"{build_id}.iso"
|
||||
|
||||
@@ -3787,7 +3827,18 @@ def run_iso_build(
|
||||
|
||||
organization = fetch_organization(organization_id)
|
||||
volid = compute_iso_volid(organization["name"] if organization else "TUXFLOTTE")
|
||||
vorlage_label = (fetch_vorlage_label_for_session(activation_code) or "").replace("\n", " ")
|
||||
vorlage_label, backend_key = fetch_vorlage_for_session(activation_code)
|
||||
vorlage_label = (vorlage_label or "").replace("\n", " ")
|
||||
golden_image_file = ""
|
||||
if offline_medium:
|
||||
golden_image = golden_image_path_for_backend(backend_key)
|
||||
if golden_image is None:
|
||||
update_iso_build(
|
||||
build_id, status="failed", mark_finished=True,
|
||||
error_message=f"Offline-Medium: kein Golden Image für Backend '{backend_key}' gefunden.",
|
||||
)
|
||||
return
|
||||
golden_image_file = str(golden_image)
|
||||
|
||||
try:
|
||||
# De-Root-Umzug (07.09.2026): der Dienst selbst laeuft nicht mehr als
|
||||
@@ -3801,7 +3852,7 @@ def run_iso_build(
|
||||
"sudo",
|
||||
"bash",
|
||||
f"{TUXFLOTTE_INSTALLER_DIR}/scripts/build_customer_iso.sh",
|
||||
MINT_ISO_PATH,
|
||||
BOOT_MEDIUM_PATH,
|
||||
str(output_path),
|
||||
activation_code,
|
||||
wifi_ssid or "",
|
||||
@@ -3815,6 +3866,7 @@ def run_iso_build(
|
||||
f"WIFI_PSK={wifi_psk or ''}\n"
|
||||
f"ROOT_PASSWORD_HASH={root_password_hash or ''}\n"
|
||||
f"VORLAGE_LABEL={vorlage_label}\n"
|
||||
f"GOLDEN_IMAGE_FILE={golden_image_file}\n"
|
||||
),
|
||||
cwd=TUXFLOTTE_INSTALLER_DIR,
|
||||
capture_output=True,
|
||||
@@ -3983,6 +4035,13 @@ def activate(payload: ActivationRequest, request: Request):
|
||||
"previously_installed": (
|
||||
False if device_created else has_completed_installation(device["id"])
|
||||
),
|
||||
# Kunden-ISO-Test 03.10.2026: pro Installationsmedium abschaltbare
|
||||
# Rueckfrage bei bereits installierten Geraeten (siehe
|
||||
# 25_installation_confirm.sh) - fuer das bewusste Neuaufsetzen
|
||||
# ganzer Raeume.
|
||||
"reinstall_without_confirmation": bool(
|
||||
enrollment_session and enrollment_session.get("neuinstallation_ohne_rueckfrage")
|
||||
),
|
||||
},
|
||||
|
||||
"customer": {
|
||||
@@ -5022,7 +5081,8 @@ def create_enrollment_session_endpoint(
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
session = create_enrollment_session(
|
||||
organization_id, payload.bereitstellungsvorlage_id, payload.max_devices, payload.expires_at
|
||||
organization_id, payload.bereitstellungsvorlage_id, payload.max_devices, payload.expires_at,
|
||||
payload.neuinstallation_ohne_rueckfrage,
|
||||
)
|
||||
|
||||
return {"success": True, "enrollment_session": session}
|
||||
@@ -5095,7 +5155,7 @@ def create_iso_build_endpoint(
|
||||
target=run_iso_build,
|
||||
args=(
|
||||
UUID(iso_build["id"]), organization_id, payload.activation_code, payload.wifi_ssid, payload.wifi_psk,
|
||||
payload.root_password_hash,
|
||||
payload.root_password_hash, payload.offline_medium,
|
||||
),
|
||||
daemon=True,
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user