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 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"],
|
||||||
|
|||||||
@ -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": [
|
||||||
|
|||||||
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