feat: add Proxmox provisioning service with CI and deployment tooling
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
from contextlib import closing, contextmanager
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations(version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);
|
||||
CREATE TABLE IF NOT EXISTS users(id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, role TEXT NOT NULL, created_at TEXT NOT NULL, disabled INTEGER NOT NULL DEFAULT 0);
|
||||
CREATE TABLE IF NOT EXISTS sessions(token_hash TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id), csrf_token TEXT NOT NULL, expires_at REAL NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS hosts(id TEXT PRIMARY KEY, fqdn TEXT NOT NULL UNIQUE COLLATE NOCASE, management_ip TEXT UNIQUE, site TEXT NOT NULL, status TEXT NOT NULL, blocked INTEGER NOT NULL DEFAULT 0, version INTEGER NOT NULL DEFAULT 1, data TEXT NOT NULL, created_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS host_identities(host_id TEXT NOT NULL REFERENCES hosts(id), kind TEXT NOT NULL, value TEXT NOT NULL, UNIQUE(kind,value));
|
||||
CREATE TABLE IF NOT EXISTS profiles(id TEXT PRIMARY KEY, name TEXT NOT NULL, kind TEXT NOT NULL, version INTEGER NOT NULL, status TEXT NOT NULL, data TEXT NOT NULL, created_by TEXT NOT NULL, created_at TEXT NOT NULL, UNIQUE(name,kind,version));
|
||||
CREATE TABLE IF NOT EXISTS modules(id TEXT PRIMARY KEY, name TEXT NOT NULL, version INTEGER NOT NULL, status TEXT NOT NULL, digest TEXT NOT NULL, data TEXT NOT NULL, created_by TEXT NOT NULL, created_at TEXT NOT NULL, UNIQUE(name,version));
|
||||
CREATE TABLE IF NOT EXISTS groups(id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, site TEXT NOT NULL, token_hash TEXT NOT NULL, expires_at REAL NOT NULL, revoked INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS iso_records(id TEXT PRIMARY KEY, name TEXT NOT NULL, data TEXT NOT NULL, created_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS secrets(id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, ciphertext TEXT NOT NULL, created_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS approvals(id TEXT PRIMARY KEY, host_id TEXT NOT NULL REFERENCES hosts(id), status TEXT NOT NULL, expires_at REAL NOT NULL, data TEXT NOT NULL, created_at TEXT NOT NULL);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS one_open_approval ON approvals(host_id) WHERE status='approved';
|
||||
CREATE TABLE IF NOT EXISTS runs(id TEXT PRIMARY KEY, host_id TEXT NOT NULL REFERENCES hosts(id), approval_id TEXT NOT NULL UNIQUE REFERENCES approvals(id), status TEXT NOT NULL, version INTEGER NOT NULL DEFAULT 1, data TEXT NOT NULL, answer_ciphertext TEXT, bootstrap_ciphertext TEXT, secrets_ciphertext TEXT, bootstrap_hash TEXT UNIQUE, enrollment_hash TEXT, report_hash TEXT UNIQUE, device_key TEXT, answer_until REAL, enroll_until REAL, lease_until REAL, last_seen REAL, completed_at REAL, created_at TEXT NOT NULL);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS one_active_run ON runs(host_id) WHERE status NOT IN ('succeeded','failed','cancelled','expired');
|
||||
CREATE TABLE IF NOT EXISTS run_steps(run_id TEXT NOT NULL REFERENCES runs(id), step_id TEXT NOT NULL, position INTEGER NOT NULL, status TEXT NOT NULL DEFAULT 'pending', attempt INTEGER NOT NULL DEFAULT 0, verification TEXT NOT NULL DEFAULT '{}', PRIMARY KEY(run_id,step_id));
|
||||
CREATE TABLE IF NOT EXISTS events(run_id TEXT NOT NULL REFERENCES runs(id), sequence INTEGER NOT NULL, data TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY(run_id,sequence));
|
||||
CREATE TABLE IF NOT EXISTS logs(run_id TEXT NOT NULL REFERENCES runs(id), sequence INTEGER NOT NULL, data TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY(run_id,sequence));
|
||||
CREATE TABLE IF NOT EXISTS nonces(run_id TEXT NOT NULL REFERENCES runs(id), nonce TEXT NOT NULL, expires_at REAL NOT NULL, PRIMARY KEY(run_id,nonce));
|
||||
CREATE TABLE IF NOT EXISTS audit(id TEXT PRIMARY KEY, actor TEXT NOT NULL, action TEXT NOT NULL, object_id TEXT NOT NULL, reason TEXT NOT NULL, data TEXT NOT NULL, created_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS discoveries(id TEXT PRIMARY KEY, fingerprint TEXT NOT NULL UNIQUE, site TEXT NOT NULL, data TEXT NOT NULL, reason TEXT NOT NULL, last_seen REAL NOT NULL);
|
||||
INSERT OR IGNORE INTO schema_migrations(version) VALUES(1);
|
||||
"""
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self, settings):
|
||||
self.path = settings.data_dir / "provisioner.sqlite3"
|
||||
|
||||
def initialize(self):
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with self.connection() as connection:
|
||||
connection.execute("PRAGMA journal_mode=WAL")
|
||||
version = connection.execute("PRAGMA user_version").fetchone()[0]
|
||||
if version > 1:
|
||||
raise RuntimeError("Datenbankschema ist neuer als diese Anwendung.")
|
||||
connection.executescript(SCHEMA)
|
||||
connection.execute("PRAGMA user_version=1")
|
||||
|
||||
@contextmanager
|
||||
def connection(self, write=False):
|
||||
connection = sqlite3.connect(self.path, timeout=15, isolation_level=None)
|
||||
connection.row_factory = sqlite3.Row
|
||||
connection.execute("PRAGMA foreign_keys=ON")
|
||||
connection.execute("PRAGMA busy_timeout=15000")
|
||||
try:
|
||||
if write:
|
||||
connection.execute("BEGIN IMMEDIATE")
|
||||
yield connection
|
||||
if connection.in_transaction:
|
||||
connection.commit()
|
||||
except BaseException:
|
||||
if connection.in_transaction:
|
||||
connection.rollback()
|
||||
raise
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def backup(self, destination: Path):
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
with self.connection() as source, closing(sqlite3.connect(destination)) as target:
|
||||
source.backup(target)
|
||||
Reference in New Issue
Block a user