devices: Installations-Meilensteine/Fehlschlaege melden (device_installation_events)

Neue Tabelle (Migration 0025) + POST /api/v1/devices/{device_id}/
installation-events (device_fingerprint-authentifiziert statt Service-
Token - das Geraet hat waehrend der Installation noch kein agent_secret,
gleiches Vertrauensniveau wie /api/v1/activate) + GET-Gegenstueck
(Service-Token, fuer die Kundenplattform).

Nutzer-Wunsch (01.09.2026, nach dem ersten echten Hardware-Test):
Fehlschlaege waehrend der Installation sichtbar machen, ohne am
Bildschirm mitschreiben zu muessen ("das ist kein Flow"). Bewusst
getrennt von device_merkmal_events (andere Lebensphase, andere Auth,
kein Merkmal-Bezug) und zunaechst nur admin-/operatorseitig ausgewertet -
ob/wie umfangreich der Kunde das in seiner eigenen GUI sieht, ist noch
offen (Nutzer: "das können wir noch wann anders diskutieren").
This commit is contained in:
Thomas Stallinger 2026-09-01 16:20:50 +02:00
parent fb87a2377a
commit 51fbf0fa0e
2 changed files with 124 additions and 0 deletions

99
app.py
View File

@ -87,6 +87,19 @@ class ResolveRequest(BaseModel):
device_id: str device_id: str
class InstallationEventRequest(BaseModel):
"""
Meilenstein/Fehlschlag-Meldung vom Boot-Medium waehrend der Installation
(siehe device_installation_events, Migration 0025) - das Geraet hat zu
diesem Zeitpunkt noch kein agent_secret, deshalb device_fingerprint als
Nachweis statt eines Service-Tokens (gleiches Vertrauensniveau wie
/api/v1/activate selbst).
"""
device_fingerprint: str
event_type: str
detail: str | None = None
class AgentBootstrapRequest(BaseModel): class AgentBootstrapRequest(BaseModel):
device_id: str device_id: str
@ -1617,6 +1630,55 @@ def fetch_device_events(device_id: str):
for occurred_at, merkmal_key, merkmal_name, event_type, detail in cur.fetchall() for occurred_at, merkmal_key, merkmal_name, event_type, detail in cur.fetchall()
] ]
def device_fingerprint_matches(device_id: str, device_fingerprint: str) -> bool:
with get_database_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT device_fingerprint FROM devices WHERE id = %s",
(device_id,),
)
row = cur.fetchone()
return row is not None and row[0] == device_fingerprint
def record_installation_event(device_id: str, event_type: str, detail: str | None) -> None:
with get_database_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO device_installation_events (id, device_id, event_type, detail)
VALUES (%s, %s, %s, %s)
""",
(uuid4(), device_id, event_type, detail),
)
def fetch_installation_events(device_id: str):
"""
Meilensteine/Fehlschlaege waehrend der Installation (siehe
device_installation_events, Migration 0025) - separat von
fetch_device_events() (Merkmal-Anwendungen), neueste zuerst.
"""
with get_database_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT occurred_at, event_type, detail
FROM device_installation_events
WHERE device_id = %s
ORDER BY occurred_at DESC
""",
(device_id,),
)
return [
{
"occurred_at": occurred_at.isoformat(),
"event_type": event_type,
"detail": detail,
}
for occurred_at, event_type, detail in cur.fetchall()
]
def deprovision_device(device_id: str) -> bool: def deprovision_device(device_id: str) -> bool:
""" """
Serverseitiger Entzug (siehe Kundenportal-Geräteaktionen-Plan): macht das Serverseitiger Entzug (siehe Kundenportal-Geräteaktionen-Plan): macht das
@ -2866,6 +2928,43 @@ def get_device_events(device_id: str, authorization: str | None = Header(default
return {"success": True, "events": fetch_device_events(device_id)} return {"success": True, "events": fetch_device_events(device_id)}
@app.post("/api/v1/devices/{device_id}/installation-events")
def post_installation_event(device_id: str, payload: InstallationEventRequest):
"""
Vom Boot-Medium selbst waehrend der Installation aufgerufen (siehe
scripts/lib/reporting.sh in tuxflotte-installer) - das Geraet hat zu
diesem Zeitpunkt noch kein agent_secret, deshalb kein Service-Token-
Zwang wie bei den uebrigen Endpunkten. device_fingerprint dient als
Nachweis, gleiches Vertrauensniveau wie /api/v1/activate selbst -
bewusst kein neues Token-System fuer einen reinen Diagnose-Kanal.
Rein melde-basiert (best effort auf Aufruferseite) - ein Fehlschlag
hier darf die eigentliche Installation nie aufhalten.
"""
if not device_fingerprint_matches(device_id, payload.device_fingerprint):
return {
"success": False,
"error": "unauthorized",
"message": "Geräte-Kennung/Fingerprint stimmen nicht überein.",
}
record_installation_event(device_id, payload.event_type, payload.detail)
return {"success": True}
@app.get("/api/v1/devices/{device_id}/installation-events")
def get_installation_events(device_id: str, authorization: str | None = Header(default=None)):
if not require_service_token(authorization):
return {
"success": False,
"error": "unauthorized",
"message": "Fehlendes oder ungültiges Service-Token.",
}
return {"success": True, "events": fetch_installation_events(device_id)}
@app.post("/api/v1/devices/{device_id}/deprovision") @app.post("/api/v1/devices/{device_id}/deprovision")
def deprovision(device_id: str, authorization: str | None = Header(default=None)): def deprovision(device_id: str, authorization: str | None = Header(default=None)):
if not require_service_token(authorization): if not require_service_token(authorization):

View File

@ -0,0 +1,25 @@
BEGIN;
-- Installations-Meilensteine/Fehlschlaege vom Boot-Medium selbst gemeldet -
-- andere Lebensphase als device_merkmal_events (laeuft schon waehrend der
-- Installation, bevor das Geraet ein agent_secret hat) und andere Datenform
-- (kein merkmal_id-Bezug), deshalb eigene Tabelle statt Wiederverwendung.
-- Nutzer-Wunsch (01.09.2026, nach dem ersten echten Hardware-Test):
-- Fehlschlaege waehrend der Installation sichtbar machen, ohne am
-- Bildschirm mitschreiben/-fotografieren zu muessen. Bewusst zunaechst nur
-- admin-/operatorseitig ausgewertet (siehe kundenplattform) - ob und wie
-- umfangreich das dem Kunden selbst gezeigt wird, ist noch offen.
CREATE TABLE device_installation_events (
id UUID PRIMARY KEY,
device_id UUID NOT NULL
REFERENCES devices(id)
ON DELETE CASCADE,
event_type TEXT NOT NULL,
detail TEXT,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_device_installation_events_device_id
ON device_installation_events(device_id);
COMMIT;