SHA256-Pruefsumme fuer ISO-Builds

Siehe Fund #7 im heutigen Self-Service-Flow-Test (Memory). Migration 0023
(iso_builds.sha256), compute_file_sha256() haest die Datei einmalig direkt
nach erfolgreichem Bau (gestreamt, nicht komplett im Speicher - ISOs sind
mehrere GB), update_iso_build() nimmt den Wert jetzt entgegen. Ermoeglicht
Integritaetspruefung unabhaengig vom Kundenplattform-Download-Proxy-Pfad.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Stallinger 2026-08-28 11:29:56 +02:00
parent 2f6ff909f9
commit e88e0da0da
2 changed files with 33 additions and 6 deletions

33
app.py
View File

@ -2141,7 +2141,7 @@ def recharge_enrollment_session(session_id: str, additional_devices: int, expire
def iso_build_row_to_dict(row) -> dict: def iso_build_row_to_dict(row) -> dict:
(build_id, organization_id, status, output_filename, error_message, (build_id, organization_id, status, output_filename, error_message,
created_at, started_at, finished_at, expires_at) = row created_at, started_at, finished_at, expires_at, sha256) = row
return { return {
"id": str(build_id), "id": str(build_id),
"organization_id": str(organization_id), "organization_id": str(organization_id),
@ -2152,6 +2152,7 @@ def iso_build_row_to_dict(row) -> dict:
"started_at": started_at.isoformat() if started_at else None, "started_at": started_at.isoformat() if started_at else None,
"finished_at": finished_at.isoformat() if finished_at else None, "finished_at": finished_at.isoformat() if finished_at else None,
"expires_at": expires_at.isoformat() if expires_at else None, "expires_at": expires_at.isoformat() if expires_at else None,
"sha256": sha256,
} }
def fetch_iso_build(build_id: str): def fetch_iso_build(build_id: str):
@ -2160,7 +2161,7 @@ def fetch_iso_build(build_id: str):
cur.execute( cur.execute(
""" """
SELECT id, organization_id, status, output_filename, error_message, SELECT id, organization_id, status, output_filename, error_message,
created_at, started_at, finished_at, expires_at created_at, started_at, finished_at, expires_at, sha256
FROM iso_builds FROM iso_builds
WHERE id = %s WHERE id = %s
""", """,
@ -2175,7 +2176,7 @@ def fetch_iso_builds(organization_id: str):
cur.execute( cur.execute(
""" """
SELECT id, organization_id, status, output_filename, error_message, SELECT id, organization_id, status, output_filename, error_message,
created_at, started_at, finished_at, expires_at created_at, started_at, finished_at, expires_at, sha256
FROM iso_builds FROM iso_builds
WHERE organization_id = %s WHERE organization_id = %s
ORDER BY created_at DESC ORDER BY created_at DESC
@ -2201,6 +2202,7 @@ def update_iso_build(
status: str, status: str,
output_filename: str | None = None, output_filename: str | None = None,
error_message: str | None = None, error_message: str | None = None,
sha256: str | None = None,
mark_started: bool = False, mark_started: bool = False,
mark_finished: bool = False, mark_finished: bool = False,
): ):
@ -2217,13 +2219,14 @@ def update_iso_build(
UPDATE iso_builds UPDATE iso_builds
SET status = %s, SET status = %s,
output_filename = COALESCE(%s, output_filename), output_filename = COALESCE(%s, output_filename),
error_message = %s error_message = %s,
sha256 = COALESCE(%s, sha256)
{", started_at = CURRENT_TIMESTAMP" if mark_started else ""} {", started_at = CURRENT_TIMESTAMP" if mark_started else ""}
{", finished_at = CURRENT_TIMESTAMP" if mark_finished else ""} {", finished_at = CURRENT_TIMESTAMP" if mark_finished else ""}
{f", expires_at = CURRENT_TIMESTAMP + INTERVAL '{ISO_BUILD_EXPIRY_DAYS} days'" if setzt_expiry else ""} {f", expires_at = CURRENT_TIMESTAMP + INTERVAL '{ISO_BUILD_EXPIRY_DAYS} days'" if setzt_expiry else ""}
WHERE id = %s WHERE id = %s
""", """,
(status, output_filename, error_message, build_id), (status, output_filename, error_message, sha256, build_id),
) )
def cleanup_old_iso_builds(organization_id: str, keep_build_id) -> None: def cleanup_old_iso_builds(organization_id: str, keep_build_id) -> None:
@ -2331,6 +2334,20 @@ def compute_iso_volid(organization_name: str) -> str:
return f"{sanitized}_{date_suffix}" return f"{sanitized}_{date_suffix}"
def compute_file_sha256(path) -> str:
"""
Streamt die Datei in 1-MB-Bloecken statt sie komplett in den Speicher zu
laden (ISOs sind mehrere GB gross) - laeuft im selben Background-Thread
wie der Bau selbst, blockiert also keinen HTTP-Request.
"""
hasher = hashlib.sha256()
with open(path, "rb") as f:
for block in iter(lambda: f.read(1024 * 1024), b""):
hasher.update(block)
return hasher.hexdigest()
def run_iso_build(build_id, organization_id: str, activation_code: str, wifi_ssid: str | None, wifi_psk: str | None) -> None: def run_iso_build(build_id, organization_id: str, activation_code: str, wifi_ssid: str | None, wifi_psk: str | None) -> None:
output_path = ISO_BUILD_OUTPUT_DIR / f"{build_id}.iso" output_path = ISO_BUILD_OUTPUT_DIR / f"{build_id}.iso"
@ -2371,7 +2388,11 @@ def run_iso_build(build_id, organization_id: str, activation_code: str, wifi_ssi
update_iso_build(build_id, status="failed", error_message=error_tail, mark_finished=True) update_iso_build(build_id, status="failed", error_message=error_tail, mark_finished=True)
return return
update_iso_build(build_id, status="completed", output_filename=output_path.name, mark_finished=True) sha256 = compute_file_sha256(output_path)
update_iso_build(
build_id, status="completed", output_filename=output_path.name, sha256=sha256, mark_finished=True
)
cleanup_old_iso_builds(organization_id, build_id) cleanup_old_iso_builds(organization_id, build_id)
@app.get("/health") @app.get("/health")

View File

@ -0,0 +1,6 @@
-- SHA256-Pruefsumme fuer heruntergeladene ISOs (28.08.2026-Fund, Self-
-- Service-Flow-Test): Download laeuft ueber den Kundenplattform-Proxy zu
-- anode, ein Hash zur Integritaetspruefung unabhaengig von diesem Pfad war
-- bislang nicht vorhanden. Einmalig direkt nach erfolgreichem Bau berechnet
-- (nicht bei jedem Seitenaufruf - mehrere GB Datei).
ALTER TABLE iso_builds ADD COLUMN sha256 TEXT;