feat: migrate device registry to PostgreSQL

This commit is contained in:
Thomas Stallinger 2026-07-12 15:22:22 +02:00
parent 8d70236ef5
commit 2683b65ffc
3 changed files with 112 additions and 23 deletions

120
app.py
View File

@ -1,6 +1,10 @@
from pathlib import Path from pathlib import Path
from datetime import datetime, timezone from datetime import datetime, timezone
import json import json
import os
from uuid import uuid4
import psycopg
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from pydantic import BaseModel from pydantic import BaseModel
@ -9,7 +13,7 @@ from fastapi.responses import FileResponse
CONFIG_PATH = Path("config.json") CONFIG_PATH = Path("config.json")
LOG_PATH = Path("activation.log") LOG_PATH = Path("activation.log")
DATABASE_URL = os.environ.get("TUXFLOTTE_DATABASE_URL")
app = FastAPI(title="Provisioning Activation Server", version="0.1.0") app = FastAPI(title="Provisioning Activation Server", version="0.1.0")
@ -30,9 +34,17 @@ def write_log(entry: dict) -> None:
with LOG_PATH.open("a", encoding="utf-8") as f: with LOG_PATH.open("a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n") f.write(json.dumps(entry, ensure_ascii=False) + "\n")
DEVICES_DIR = Path("data/devices") def get_database_connection():
if not DATABASE_URL:
raise RuntimeError("TUXFLOTTE_DATABASE_URL ist nicht gesetzt.")
def load_or_create_device(device_fingerprint: str) -> tuple[dict, bool]: return psycopg.connect(DATABASE_URL)
def load_or_create_device(
organization_id,
device_fingerprint: str,
hostname: str | None,
) -> tuple[dict, bool]:
""" """
Lädt ein bekanntes Gerät oder legt es neu an. Lädt ein bekanntes Gerät oder legt es neu an.
@ -40,33 +52,90 @@ def load_or_create_device(device_fingerprint: str) -> tuple[dict, bool]:
(device, created) (device, created)
""" """
DEVICES_DIR.mkdir(parents=True, exist_ok=True) with get_database_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
id,
organization_id,
device_fingerprint,
hostname,
created_at,
last_seen
FROM devices
WHERE device_fingerprint = %s
""",
(device_fingerprint,),
)
device_file = DEVICES_DIR / f"{device_fingerprint}.json" row = cur.fetchone()
if device_file.exists(): if row is not None:
with device_file.open("r", encoding="utf-8") as f: if str(row[1]) != str(organization_id):
device = json.load(f) raise RuntimeError(
"Das Gerät ist bereits einer anderen Organization zugeordnet."
)
cur.execute(
"""
UPDATE devices
SET
hostname = %s,
last_seen = CURRENT_TIMESTAMP
WHERE id = %s
RETURNING
id,
organization_id,
device_fingerprint,
hostname,
created_at,
last_seen
""",
(hostname, row[0]),
)
device["last_seen"] = datetime.now(timezone.utc).isoformat() row = cur.fetchone()
device["activation_count"] = device.get("activation_count", 0) + 1 created = False
else:
device_id = uuid4()
with device_file.open("w", encoding="utf-8") as f: cur.execute(
json.dump(device, f, indent=2) """
INSERT INTO devices (
id,
organization_id,
device_fingerprint,
hostname
)
VALUES (%s, %s, %s, %s)
RETURNING
id,
device_fingerprint,
hostname,
created_at,
last_seen
""",
(
device_id,
organization_id,
device_fingerprint,
hostname,
),
)
return device, False row = cur.fetchone()
created = True
device = { device = {
"device_fingerprint": device_fingerprint, "id": str(row[0]),
"created_at": datetime.now(timezone.utc).isoformat(), "organization_id": str(row[1]),
"last_seen": datetime.now(timezone.utc).isoformat(), "device_fingerprint": row[1],
"activation_count": 1 "hostname": row[2],
"created_at": row[3].isoformat(),
"last_seen": row[4].isoformat(),
} }
with device_file.open("w", encoding="utf-8") as f: return device, created
json.dump(device, f, indent=2)
return device, True
@app.get("/health") @app.get("/health")
def health(): def health():
@ -87,8 +156,12 @@ def activate(payload: ActivationRequest, request: Request):
device_created = False device_created = False
if success: if success:
organization_id = activation_entry["customer"]["organization_id"]
device, device_created = load_or_create_device( device, device_created = load_or_create_device(
payload.device_fingerprint organization_id,
payload.device_fingerprint,
payload.hostname,
) )
write_log({ write_log({
@ -120,7 +193,10 @@ def activate(payload: ActivationRequest, request: Request):
"success": True, "success": True,
"device": { "device": {
"id": device["id"],
"fingerprint": device["device_fingerprint"], "fingerprint": device["device_fingerprint"],
"hostname": device["hostname"],
"created": device_created,
}, },
"customer": activation_entry["customer"], "customer": activation_entry["customer"],

View File

@ -3,6 +3,7 @@
"LAB-2026-START": { "LAB-2026-START": {
"customer": { "customer": {
"id": "default", "id": "default",
"organization_id": "11111111-1111-4111-8111-111111111111",
"name": "Default Lab" "name": "Default Lab"
}, },
"profile_ids": [ "profile_ids": [

View File

@ -0,0 +1,12 @@
BEGIN;
INSERT INTO organizations (
id,
name
)
VALUES (
'11111111-1111-4111-8111-111111111111',
'Default Lab'
);
COMMIT;