feat: migrate device registry to PostgreSQL
This commit is contained in:
parent
8d70236ef5
commit
2683b65ffc
120
app.py
120
app.py
@ -1,6 +1,10 @@
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
import os
|
||||
from uuid import uuid4
|
||||
|
||||
import psycopg
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from pydantic import BaseModel
|
||||
@ -9,7 +13,7 @@ from fastapi.responses import FileResponse
|
||||
|
||||
CONFIG_PATH = Path("config.json")
|
||||
LOG_PATH = Path("activation.log")
|
||||
|
||||
DATABASE_URL = os.environ.get("TUXFLOTTE_DATABASE_URL")
|
||||
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:
|
||||
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.
|
||||
|
||||
@ -40,33 +52,90 @@ def load_or_create_device(device_fingerprint: str) -> tuple[dict, bool]:
|
||||
(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():
|
||||
with device_file.open("r", encoding="utf-8") as f:
|
||||
device = json.load(f)
|
||||
if row is not None:
|
||||
if str(row[1]) != str(organization_id):
|
||||
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()
|
||||
device["activation_count"] = device.get("activation_count", 0) + 1
|
||||
row = cur.fetchone()
|
||||
created = False
|
||||
else:
|
||||
device_id = uuid4()
|
||||
|
||||
with device_file.open("w", encoding="utf-8") as f:
|
||||
json.dump(device, f, indent=2)
|
||||
cur.execute(
|
||||
"""
|
||||
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_fingerprint": device_fingerprint,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
"last_seen": datetime.now(timezone.utc).isoformat(),
|
||||
"activation_count": 1
|
||||
"id": str(row[0]),
|
||||
"organization_id": str(row[1]),
|
||||
"device_fingerprint": row[1],
|
||||
"hostname": row[2],
|
||||
"created_at": row[3].isoformat(),
|
||||
"last_seen": row[4].isoformat(),
|
||||
}
|
||||
|
||||
with device_file.open("w", encoding="utf-8") as f:
|
||||
json.dump(device, f, indent=2)
|
||||
|
||||
return device, True
|
||||
return device, created
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
@ -87,8 +156,12 @@ def activate(payload: ActivationRequest, request: Request):
|
||||
device_created = False
|
||||
|
||||
if success:
|
||||
organization_id = activation_entry["customer"]["organization_id"]
|
||||
|
||||
device, device_created = load_or_create_device(
|
||||
payload.device_fingerprint
|
||||
organization_id,
|
||||
payload.device_fingerprint,
|
||||
payload.hostname,
|
||||
)
|
||||
|
||||
write_log({
|
||||
@ -120,7 +193,10 @@ def activate(payload: ActivationRequest, request: Request):
|
||||
"success": True,
|
||||
|
||||
"device": {
|
||||
"id": device["id"],
|
||||
"fingerprint": device["device_fingerprint"],
|
||||
"hostname": device["hostname"],
|
||||
"created": device_created,
|
||||
},
|
||||
|
||||
"customer": activation_entry["customer"],
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"LAB-2026-START": {
|
||||
"customer": {
|
||||
"id": "default",
|
||||
"organization_id": "11111111-1111-4111-8111-111111111111",
|
||||
"name": "Default Lab"
|
||||
},
|
||||
"profile_ids": [
|
||||
|
||||
12
migrations/0002_seed_default_organization.sql
Normal file
12
migrations/0002_seed_default_organization.sql
Normal file
@ -0,0 +1,12 @@
|
||||
BEGIN;
|
||||
|
||||
INSERT INTO organizations (
|
||||
id,
|
||||
name
|
||||
)
|
||||
VALUES (
|
||||
'11111111-1111-4111-8111-111111111111',
|
||||
'Default Lab'
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
Loading…
x
Reference in New Issue
Block a user