wg-easy + Traefik docker-compose stack (Phase 1) plus a stdlib-only control-plane API for box registration, WireGuard peer provisioning via wg-easy, and per-box route publish/unpublish backed by Traefik's file provider (Phase 2, single-tenant mode). SQLite holds accounts/boxes/routes so a later multi-tenant shared instance is the same schema with more rows, not a reshape. wg-easy's actual REST API was verified against its source rather than assumed: it has no bearer-token auth (session-cookie login via POST /api/auth/password) and no way to accept an externally-generated public key (it always mints the keypair itself, private key included) — both corrected from the original plan during implementation.
55 lines
1.9 KiB
Python
55 lines
1.9 KiB
Python
"""Thin sqlite3 wrapper: one connection per process, guarded by a lock.
|
|
|
|
Why sqlite3 instead of furtka-core's flat-JSON-plus-flock convention: that
|
|
convention works because each file is effectively single-writer. This
|
|
gateway's defining requirement is many unrelated accounts/boxes registering
|
|
and publishing routes concurrently — a locked JSON file would serialize
|
|
every tenant's writes against every other tenant's. sqlite3 (stdlib, zero
|
|
extra dependencies) gives real concurrent relational access instead.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
import threading
|
|
from pathlib import Path
|
|
|
|
from control_plane import paths
|
|
|
|
_SCHEMA_PATH = Path(__file__).parent / "schema.sql"
|
|
|
|
|
|
class Database:
|
|
def __init__(self, db_path: Path | None = None) -> None:
|
|
path = db_path or paths.db_path()
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
self._lock = threading.Lock()
|
|
self._conn = sqlite3.connect(path, check_same_thread=False)
|
|
self._conn.row_factory = sqlite3.Row
|
|
self._conn.execute("PRAGMA foreign_keys = ON")
|
|
with self._lock:
|
|
self._conn.executescript(_SCHEMA_PATH.read_text())
|
|
self._conn.commit()
|
|
|
|
def execute(self, sql: str, params: tuple = ()) -> None:
|
|
with self._lock:
|
|
self._conn.execute(sql, params)
|
|
self._conn.commit()
|
|
|
|
def query_one(self, sql: str, params: tuple = ()) -> sqlite3.Row | None:
|
|
with self._lock:
|
|
return self._conn.execute(sql, params).fetchone()
|
|
|
|
def query_all(self, sql: str, params: tuple = ()) -> list[sqlite3.Row]:
|
|
with self._lock:
|
|
return self._conn.execute(sql, params).fetchall()
|
|
|
|
def close(self) -> None:
|
|
with self._lock:
|
|
self._conn.close()
|
|
|
|
|
|
def row_to_dict(row: sqlite3.Row | None) -> dict | None:
|
|
if row is None:
|
|
return None
|
|
return dict(zip(row.keys(), row, strict=True))
|