Compare commits

..

No commits in common. "e02b646564f039e4f60a851ce29f41da6b94d884" and "02e2b9de943c118e2428ede755d8890a30561189" have entirely different histories.

7 changed files with 100 additions and 277 deletions

2
.gitignore vendored
View File

@ -2,3 +2,5 @@
__pycache__/
*.pyc
activation.log
data/devices/*.json

170
app.py
View File

@ -11,6 +11,7 @@ from pydantic import BaseModel
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")
@ -25,8 +26,9 @@ class ActivationRequest(BaseModel):
hardware: dict
class ResolveRequest(BaseModel):
device_id: str
def load_config() -> dict:
with CONFIG_PATH.open("r", encoding="utf-8") as f:
return json.load(f)
def write_log(entry: dict) -> None:
@ -253,55 +255,6 @@ def store_hardware_snapshot(
return str(snapshot_id)
def find_activation_code(code: str):
with get_database_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT ac.organization_id, o.name
FROM activation_codes ac
JOIN organizations o ON o.id = ac.organization_id
WHERE ac.code = %s AND ac.active
""",
(code,),
)
row = cur.fetchone()
if row is None:
return None
return {"organization_id": str(row[0]), "organization_name": row[1]}
def fetch_templates(organization_id: str) -> list[dict]:
with get_database_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
bv.id, bv.label, bv.is_default,
w.id, w.name,
b.id, b.name, b.version
FROM bereitstellungsvorlagen bv
JOIN workspaces w ON w.id = bv.workspace_id
JOIN backends b ON b.id = bv.backend_id
WHERE bv.organization_id = %s
ORDER BY bv.is_default DESC, bv.label
""",
(organization_id,),
)
rows = cur.fetchall()
return [
{
"id": str(row[0]),
"label": row[1],
"workspace": {"id": str(row[3]), "name": row[4]},
"backend": {"id": str(row[5]), "name": row[6], "version": row[7]},
"is_default": row[2],
}
for row in rows
]
@app.get("/health")
def health():
return {
@ -313,16 +266,17 @@ def health():
@app.post("/api/v1/activate")
def activate(payload: ActivationRequest, request: Request):
activation = find_activation_code(payload.activation_code)
config = load_config()
activation_entry = config.get("activation_codes", {}).get(payload.activation_code)
success = activation is not None
success = activation_entry is not None
device = None
device_created = False
hardware_snapshot_id = None
if success:
organization_id = activation["organization_id"]
organization_id = activation_entry["customer"]["organization_id"]
device, device_created = load_or_create_device(
organization_id,
@ -352,6 +306,13 @@ def activate(payload: ActivationRequest, request: Request):
"message": "Der Aktivierungscode ist ungültig."
}
profiles = config.get("profiles", {})
available_profiles = [
profiles[profile_id]
for profile_id in activation_entry.get("profile_ids", [])
if profile_id in profiles
]
return {
"success": True,
@ -367,81 +328,56 @@ def activate(payload: ActivationRequest, request: Request):
"hardware_snapshot_id": hardware_snapshot_id,
},
"customer": {
"id": activation["organization_id"],
"organization_id": activation["organization_id"],
"name": activation["organization_name"],
},
"templates": fetch_templates(activation["organization_id"]),
"customer": activation_entry["customer"],
"profiles": available_profiles
}
@app.post("/api/v1/templates/{template_id}/resolve")
def resolve_template(template_id: str, payload: ResolveRequest):
with get_database_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT bv.workspace_id, bv.backend_id, w.key, b.key,
bv.disk_encryption, bv.partitioning, bv.secure_boot_required
FROM bereitstellungsvorlagen bv
JOIN workspaces w ON w.id = bv.workspace_id
JOIN backends b ON b.id = bv.backend_id
WHERE bv.id = %s
""",
(template_id,),
)
template_row = cur.fetchone()
@app.get("/api/v1/profiles/{profile_id}")
def get_profile(profile_id: str):
config = load_config()
profile = config.get("profiles", {}).get(profile_id)
if template_row is None:
if not profile:
return {
"success": False,
"error": "template_not_found",
"message": "Die angeforderte Bereitstellungsvorlage wurde nicht gefunden."
"error": "profile_not_found",
"message": "Das angeforderte Profil wurde nicht gefunden."
}
(
workspace_id, backend_id, workspace_key, backend_key,
disk_encryption, partitioning, secure_boot_required,
) = template_row
cur.execute(
"""
SELECT m.key, bp.ansible_role
FROM workspace_merkmale wm
JOIN merkmale m ON m.id = wm.merkmal_id
JOIN blueprints bp ON bp.merkmal_id = m.id AND bp.backend_id = %s
WHERE wm.workspace_id = %s
""",
(backend_id, workspace_id),
)
blueprints = [
{"merkmal": key, "ansible_role": ansible_role}
for key, ansible_role in cur.fetchall()
]
cur.execute(
"""
INSERT INTO assignments (id, device_id, bereitstellungsvorlage_id)
VALUES (%s, %s, %s)
ON CONFLICT (device_id) DO UPDATE
SET bereitstellungsvorlage_id = EXCLUDED.bereitstellungsvorlage_id
""",
(uuid4(), payload.device_id, template_id),
)
return {
"success": True,
"runtime_blueprint": {
"workspace_id": workspace_key,
"backend_id": backend_key,
"blueprints": blueprints,
"installation_directives": {
"disk_encryption": disk_encryption,
"partitioning": partitioning,
"secure_boot_required": secure_boot_required,
"profile": profile
}
@app.get("/api/v1/profiles/{profile_id}/bootstrap")
def get_profile_bootstrap(profile_id: str):
config = load_config()
profile = config.get("profiles", {}).get(profile_id)
if not profile:
return {
"success": False,
"error": "profile_not_found",
"message": "Das angeforderte Profil wurde nicht gefunden."
}
return {
"success": True,
"profile": {
"id": profile["id"],
"label": profile["label"],
"distribution": profile["distribution"],
"version": profile["version"]
},
"installer": profile.get("installer"),
"ansible": {
"repo": profile.get("ansible_repo"),
"mode": "pull"
},
"ssh": {
"authorized_keys_url": "https://anode.tuxflotte.de/keys/default.pub"
}
}
@app.get("/installers/fedora-workstation/ks.cfg")

41
config.json Normal file
View File

@ -0,0 +1,41 @@
{
"activation_codes": {
"LAB-2026-START": {
"customer": {
"id": "default",
"organization_id": "11111111-1111-4111-8111-111111111111",
"name": "Default Lab"
},
"profile_ids": [
"mint-desktop",
"fedora-workstation"
]
}
},
"profiles": {
"mint-desktop": {
"id": "mint-desktop",
"label": "Linux Mint Desktop",
"distribution": "linux-mint",
"version": "22",
"description": "Standard-Desktop-Profil für Linux Mint.",
"ansible_repo": "https://git.example.com/stallinux/mint-desktop.git",
"installer": {
"type": "autoinstall",
"url": "https://anode.tuxflotte.de/installers/mint-desktop/user-data"
}
},
"fedora-workstation": {
"id": "fedora-workstation",
"label": "Fedora Workstation",
"distribution": "fedora",
"version": "40",
"description": "Standard-Workstation-Profil für Fedora.",
"ansible_repo": "https://git.example.com/stallinux/fedora-workstation.git",
"installer": {
"type": "kickstart",
"url": "https://anode.tuxflotte.de/installers/fedora-workstation/ks.cfg"
}
}
}
}

0
data/devices/.gitkeep Normal file
View File

View File

@ -1,96 +0,0 @@
BEGIN;
CREATE TABLE workspaces (
id UUID PRIMARY KEY,
organization_id UUID
REFERENCES organizations(id)
ON DELETE CASCADE,
key TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (organization_id, key)
);
CREATE TABLE merkmale (
id UUID PRIMARY KEY,
key TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
description TEXT
);
CREATE TABLE workspace_merkmale (
workspace_id UUID NOT NULL
REFERENCES workspaces(id)
ON DELETE CASCADE,
merkmal_id UUID NOT NULL
REFERENCES merkmale(id)
ON DELETE RESTRICT,
PRIMARY KEY (workspace_id, merkmal_id)
);
CREATE TABLE backends (
id UUID PRIMARY KEY,
key TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
installer_type TEXT NOT NULL
);
CREATE TABLE blueprints (
id UUID PRIMARY KEY,
merkmal_id UUID NOT NULL
REFERENCES merkmale(id)
ON DELETE CASCADE,
backend_id UUID NOT NULL
REFERENCES backends(id)
ON DELETE CASCADE,
ansible_role TEXT NOT NULL,
UNIQUE (merkmal_id, backend_id)
);
CREATE TABLE bereitstellungsvorlagen (
id UUID PRIMARY KEY,
organization_id UUID NOT NULL
REFERENCES organizations(id)
ON DELETE CASCADE,
workspace_id UUID NOT NULL
REFERENCES workspaces(id)
ON DELETE RESTRICT,
backend_id UUID NOT NULL
REFERENCES backends(id)
ON DELETE RESTRICT,
label TEXT NOT NULL,
is_default BOOLEAN NOT NULL DEFAULT FALSE,
disk_encryption BOOLEAN NOT NULL DEFAULT FALSE,
partitioning TEXT NOT NULL DEFAULT 'default',
secure_boot_required BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX bereitstellungsvorlagen_one_default_per_org
ON bereitstellungsvorlagen (organization_id)
WHERE is_default;
CREATE TABLE assignments (
id UUID PRIMARY KEY,
device_id UUID NOT NULL UNIQUE
REFERENCES devices(id)
ON DELETE CASCADE,
bereitstellungsvorlage_id UUID NOT NULL
REFERENCES bereitstellungsvorlagen(id)
ON DELETE RESTRICT,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE activation_codes (
code TEXT PRIMARY KEY,
organization_id UUID NOT NULL
REFERENCES organizations(id)
ON DELETE CASCADE,
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMIT;

View File

@ -1,5 +0,0 @@
BEGIN;
ALTER TABLE backends ADD COLUMN version TEXT;
COMMIT;

View File

@ -1,55 +0,0 @@
BEGIN;
-- Backends
INSERT INTO backends (id, key, name, installer_type, version) VALUES
('22222222-2222-4222-8222-222222222222', 'fedora', 'Fedora', 'kickstart', '40'),
('33333333-3333-4333-8333-333333333333', 'mint', 'Linux Mint', 'autoinstall', '22');
-- Workspaces (global, organization_id NULL)
INSERT INTO workspaces (id, organization_id, key, name, description) VALUES
('44444444-4444-4444-8444-444444444444', NULL, 'schulcomputer', 'Schulcomputer',
'Arbeitsplatz fuer den Schuleinsatz mit fluechtiger Gastsitzung.'),
('55555555-5555-4555-8555-555555555555', NULL, 'entwickler', 'Entwickler',
'Arbeitsplatz fuer Softwareentwicklung. Merkmale noch nicht konkretisiert.');
-- Merkmale (bisher nur fuer Schulcomputer konkretisiert)
INSERT INTO merkmale (id, key, name, description) VALUES
('66666666-6666-4666-8666-666666666666', 'guest-session-ephemeral', 'Fluechtige Gastsitzung',
'Loescht Dateien, Cache und Verlauf beim Abmelden.'),
('77777777-7777-4777-8777-777777777777', 'browser-brave', 'Brave Browser', NULL),
('88888888-8888-4888-8888-888888888888', 'office-onlyoffice', 'OnlyOffice', NULL);
INSERT INTO workspace_merkmale (workspace_id, merkmal_id) VALUES
('44444444-4444-4444-8444-444444444444', '66666666-6666-4666-8666-666666666666'),
('44444444-4444-4444-8444-444444444444', '77777777-7777-4777-8777-777777777777'),
('44444444-4444-4444-8444-444444444444', '88888888-8888-4888-8888-888888888888');
-- Blueprints je Merkmal x Backend (Ansible-Rollen aus ADR-0002 uebernommen bzw. nach demselben Schema ergaenzt)
INSERT INTO blueprints (id, merkmal_id, backend_id, ansible_role) VALUES
('b1000000-0000-4000-8000-000000000001', '66666666-6666-4666-8666-666666666666', '22222222-2222-4222-8222-222222222222', 'guest-session'),
('b1000000-0000-4000-8000-000000000002', '66666666-6666-4666-8666-666666666666', '33333333-3333-4333-8333-333333333333', 'guest-session'),
('b1000000-0000-4000-8000-000000000003', '77777777-7777-4777-8777-777777777777', '22222222-2222-4222-8222-222222222222', 'brave-fedora'),
('b1000000-0000-4000-8000-000000000004', '77777777-7777-4777-8777-777777777777', '33333333-3333-4333-8333-333333333333', 'brave-mint'),
('b1000000-0000-4000-8000-000000000005', '88888888-8888-4888-8888-888888888888', '22222222-2222-4222-8222-222222222222', 'onlyoffice-fedora'),
('b1000000-0000-4000-8000-000000000006', '88888888-8888-4888-8888-888888888888', '33333333-3333-4333-8333-333333333333', 'onlyoffice-mint');
-- Bereitstellungsvorlagen fuer die bestehende "Default Lab"-Organisation
INSERT INTO bereitstellungsvorlagen (id, organization_id, workspace_id, backend_id, label, is_default) VALUES
('c1000000-0000-4000-8000-000000000001', '11111111-1111-4111-8111-111111111111',
'44444444-4444-4444-8444-444444444444', '22222222-2222-4222-8222-222222222222',
'Schulcomputer (Fedora)', TRUE),
('c1000000-0000-4000-8000-000000000002', '11111111-1111-4111-8111-111111111111',
'44444444-4444-4444-8444-444444444444', '33333333-3333-4333-8333-333333333333',
'Schulcomputer (Mint)', FALSE);
-- Aktivierungscode (Ablösung des bisherigen config.json-Eintrags)
INSERT INTO activation_codes (code, organization_id, active) VALUES
('LAB-2026-START', '11111111-1111-4111-8111-111111111111', TRUE);
COMMIT;