#!/usr/bin/env python3 """Initialisiert die SQLite-Datenbank für TESM. WICHTIG: Wer hier eine neue Tabelle oder Spalte hinzufügt, muss auch die Datei SCHEMA_VERSION (daneben, im selben Verzeichnis) um 1 erhöhen. update.sh vergleicht diese Zahl zwischen der aktuell installierten und der neuen Version, um zu entscheiden, ob ein Update in-place laufen darf (bestehende Datenbank bleibt erhalten) oder eine frische Installation nötig ist (Datenbank wird beiseitegesichert). Ein vergessenes Hochzählen würde dazu führen, dass ein Host mit altem Schema faelschlich per In-Place-Update aktualisiert wird, statt frisch installiert zu werden -- _ensure_schema() faengt die meisten Faelle zwar zur Laufzeit ab, ist aber kein Ersatz fuer eine korrekte Versionsnummer hier.""" import os import sqlite3 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_PATH = os.environ.get("TESM_DB_PATH", os.path.join(BASE_DIR, "sqlite.db")) conn = sqlite3.connect(DB_PATH) c = conn.cursor() # Zugangsdaten (wiederverwendbare SSH-Logins, mehrere Switche können sich # dieselben teilen; Passwort wird verschlüsselt gespeichert) c.execute(""" CREATE TABLE IF NOT EXISTS credentials ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, username TEXT NOT NULL, password TEXT NOT NULL, category TEXT NOT NULL DEFAULT 'switch', deleted_at TEXT ); """) # Switches (Aruba-Switche) — referenzieren ihre Zugangsdaten statt sie # direkt zu speichern c.execute(""" CREATE TABLE IF NOT EXISTS switches ( hostname TEXT PRIMARY KEY, ip TEXT NOT NULL, ssh_port INTEGER, credential_id INTEGER, last_modified_by TEXT, last_modified_at TEXT, deleted_at TEXT, FOREIGN KEY (credential_id) REFERENCES credentials(id) ); """) # Devices c.execute(""" CREATE TABLE IF NOT EXISTS devices ( mac TEXT PRIMARY KEY, ip TEXT NOT NULL, switch_hostname TEXT, port TEXT, name TEXT NOT NULL, is_active INTEGER DEFAULT 1, last_modified_by TEXT, last_modified_at TEXT, credential_id INTEGER, ssh_port INTEGER, deleted_at TEXT, FOREIGN KEY (switch_hostname) REFERENCES switches(hostname) ); """) # Benutzer c.execute(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, is_admin INTEGER DEFAULT 0, first_name TEXT, last_name TEXT, avatar_filename TEXT, auth_source TEXT NOT NULL DEFAULT 'local', email TEXT, is_locked INTEGER NOT NULL DEFAULT 0, deleted_at TEXT ); """) # Gruppen (Rechtesystem) — Admins besitzen ohnehin alle Rechte; Gruppen # dienen dazu, normalen Benutzern gezielt einzelne Verwaltungsrechte für # Devices/Switches zu geben, ohne sie zu Admins zu machen. c.execute(""" CREATE TABLE IF NOT EXISTS groups ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, is_default INTEGER DEFAULT 0, is_system INTEGER DEFAULT 0, deleted_at TEXT ); """) # Welche Berechtigungen eine Gruppe gewährt (siehe app.py: PERMISSIONS) c.execute(""" CREATE TABLE IF NOT EXISTS group_permissions ( group_id INTEGER NOT NULL, permission TEXT NOT NULL, PRIMARY KEY (group_id, permission), FOREIGN KEY (group_id) REFERENCES groups(id) ); """) # Gruppenmitgliedschaft (ein Benutzer kann in mehreren Gruppen sein; # effektive Rechte = Vereinigung aller Gruppen) c.execute(""" CREATE TABLE IF NOT EXISTS user_groups ( user_id INTEGER NOT NULL, group_id INTEGER NOT NULL, PRIMARY KEY (user_id, group_id), FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (group_id) REFERENCES groups(id) ); """) # Settings (z.B. Prüfintervall) c.execute(""" CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); """) # DHCP: eigene Options-Definitionen (analog "Vordefinierte Optionen und # Werte" bei einem Windows-DHCP-Server) — Code/Name/Typ/Beschreibung. # is_standard markiert die vorbefüllten Standard-Optionen (Router/DNS/…) -- # die können vom Admin nicht gelöscht werden, nur ihr Wert geleert (die # tatsächliche Vorbefüllung übernimmt app.py: _ensure_schema()). c.execute(""" CREATE TABLE IF NOT EXISTS dhcp_option_defs ( id INTEGER PRIMARY KEY AUTOINCREMENT, code INTEGER NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE, type TEXT NOT NULL, description TEXT, is_standard INTEGER NOT NULL DEFAULT 0 ); """) # DHCP: Werte je Option — device_mac='' bedeutet global (gilt für den # ganzen Scope), ein gesetzter device_mac überschreibt den globalen Wert # für genau dieses Gerät. Bewusst '' statt NULL für den globalen Fall, # da SQLite NULL in UNIQUE-Constraints nicht als gleich behandelt (zwei # NULL-Zeilen wären sonst kein Konflikt) — mit '' funktioniert ON CONFLICT # zuverlässig für den Upsert des globalen Werts. c.execute(""" CREATE TABLE IF NOT EXISTS dhcp_option_values ( id INTEGER PRIMARY KEY AUTOINCREMENT, option_def_id INTEGER NOT NULL, device_mac TEXT NOT NULL DEFAULT '', value TEXT NOT NULL, FOREIGN KEY (option_def_id) REFERENCES dhcp_option_defs(id), UNIQUE (option_def_id, device_mac) ); """) # DHCP: konfigurierte Subnetze (Interface + Range + optionales Gateway/DNS) # -- ein Host kann mehrere Subnetze gleichzeitig bedienen, je eines pro # Interface/Netz. sort_order steuert die Anzeige-/Verarbeitungsreihenfolge, # enabled erlaubt ein Subnetz vorübergehend aus der geschriebenen # Konfiguration auszuschließen, ohne es zu löschen. c.execute(""" CREATE TABLE IF NOT EXISTS dhcp_subnets ( id INTEGER PRIMARY KEY AUTOINCREMENT, interface TEXT NOT NULL, range_start TEXT NOT NULL, range_end TEXT NOT NULL, gateway TEXT, dns TEXT, sort_order INTEGER NOT NULL DEFAULT 0, enabled INTEGER NOT NULL DEFAULT 1 ); """) # DHCP: manuelle Reservierungen für Geräte OHNE eigenen Client-Datensatz # (Geräte mit MAC+IP in der devices-Tabelle bekommen ihre Reservierung # automatisch, siehe _dhcp_reservation_candidates in app.py). c.execute(""" CREATE TABLE IF NOT EXISTS dhcp_manual_reservations ( id INTEGER PRIMARY KEY AUTOINCREMENT, mac TEXT NOT NULL UNIQUE, ip TEXT NOT NULL, name TEXT NOT NULL ); """) # Service-Accounts (aktuell nur das LDAP-Bind-Konto) -- bewusst NICHT in der # users-Tabelle (kein echter, einloggbarer App-Benutzer) und NICHT in der # credentials-Tabelle (die ist für Switch-/Geräte-Zugangsdaten, ein anderer # Anwendungsfall) -- eine eigene, kleine Tabelle. "purpose" ist UNIQUE, ein # neu gespeichertes Konto für denselben Zweck ersetzt das alte. c.execute(""" CREATE TABLE IF NOT EXISTS service_accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, purpose TEXT UNIQUE NOT NULL, username TEXT NOT NULL, password TEXT NOT NULL ); """) # AD-Gruppe -> App-Rechtegruppe: mehrere AD-Gruppen können jeweils einer # App-Gruppe zugeordnet werden (additiv, siehe _ldap_resolve_app_groups in # app.py) -- passt keine dieser Gruppen, greift die Standardgruppe. c.execute(""" CREATE TABLE IF NOT EXISTS ldap_group_mappings ( id INTEGER PRIMARY KEY AUTOINCREMENT, ad_group_dn TEXT UNIQUE NOT NULL, ad_group_name TEXT NOT NULL, app_group_id INTEGER NOT NULL, FOREIGN KEY (app_group_id) REFERENCES groups(id) ); """) # Änderungslog (Audit-Log) — wer hat wann was geändert. Bewusst OHNE # PoE-Neustarts (die stehen im Live-Log von poe.sh). c.execute(""" CREATE TABLE IF NOT EXISTS audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT NOT NULL, username TEXT NOT NULL, action TEXT NOT NULL, target TEXT, details TEXT ); """) # Standard-Setting: Prüfintervall 5 Minuten (interval = Minuten, check_interval = Sekunden) c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("interval", "5")) c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("check_interval", "300")) # Standardgruppe 'Benutzer' — Systemgruppe (is_system), Name und Rechte # sind über die UI fest und nicht änderbar. Jeder neu angelegte (nicht-admin) # Benutzer landet automatisch darin (siehe app.py: users()). Der genaue # Rechtesatz muss mit DEFAULT_GROUP_PERMISSIONS in app.py übereinstimmen. cur = c.execute("INSERT OR IGNORE INTO groups (name, is_default, is_system) VALUES (?, 1, 1)", ("Benutzer",)) if cur.rowcount > 0: default_group_id = cur.lastrowid c.executemany( "INSERT OR IGNORE INTO group_permissions (group_id, permission) VALUES (?, ?)", [ (default_group_id, "devices_group.view"), (default_group_id, "devices.view"), (default_group_id, "switches.view"), (default_group_id, "logs_group.view"), (default_group_id, "logs_live.view"), ], ) conn.commit() conn.close() print(f"Datenbank '{DB_PATH}' wurde initialisiert inklusive Settings.")