package db import ( "database/sql" "encoding/json" "fmt" "time" "github.com/cynfo/rmm-backend/models" _ "modernc.org/sqlite" ) type Database struct { db *sql.DB } func New(dbPath string) (*Database, error) { db, err := sql.Open("sqlite", dbPath) if err != nil { return nil, fmt.Errorf("Datenbank oeffnen: %w", err) } // WAL-Modus fuer bessere Performance if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil { return nil, err } d := &Database{db: db} if err := d.migrate(); err != nil { return nil, fmt.Errorf("Migration fehlgeschlagen: %w", err) } return d, nil } func (d *Database) migrate() error { schema := ` CREATE TABLE IF NOT EXISTS agents ( id TEXT PRIMARY KEY, name TEXT NOT NULL, hostname TEXT NOT NULL, ip TEXT NOT NULL, opnsense_version TEXT NOT NULL DEFAULT '', registered_at TEXT NOT NULL, last_heartbeat TEXT ); CREATE TABLE IF NOT EXISTS config_backups ( id INTEGER PRIMARY KEY AUTOINCREMENT, agent_id TEXT NOT NULL, hash TEXT NOT NULL, size INTEGER NOT NULL, config_xml TEXT NOT NULL, created_at TEXT NOT NULL, FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_config_backups_agent ON config_backups(agent_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_config_backups_hash ON config_backups(agent_id, hash); CREATE TABLE IF NOT EXISTS system_data ( agent_id TEXT PRIMARY KEY, data_json TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE ); ` _, err := d.db.Exec(schema) return err } func (d *Database) Close() error { return d.db.Close() } // RegisterAgent - Neuen Agent registrieren oder bestehenden aktualisieren func (d *Database) RegisterAgent(agent *models.Agent) error { _, err := d.db.Exec(` INSERT INTO agents (id, name, hostname, ip, opnsense_version, registered_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET name=excluded.name, hostname=excluded.hostname, ip=excluded.ip, opnsense_version=excluded.opnsense_version `, agent.ID, agent.Name, agent.Hostname, agent.IP, agent.OPNsenseVersion, agent.RegisteredAt.UTC().Format(time.RFC3339)) return err } // SaveSystemData - Systemdaten speichern und Heartbeat aktualisieren func (d *Database) SaveSystemData(agentID string, data *models.SystemData) error { now := time.Now().UTC().Format(time.RFC3339) jsonData, err := json.Marshal(data) if err != nil { return fmt.Errorf("JSON Marshal: %w", err) } tx, err := d.db.Begin() if err != nil { return err } defer tx.Rollback() // Heartbeat Zeitstempel aktualisieren res, err := tx.Exec("UPDATE agents SET last_heartbeat = ? WHERE id = ?", now, agentID) if err != nil { return err } rows, _ := res.RowsAffected() if rows == 0 { return fmt.Errorf("Agent %s nicht gefunden", agentID) } // Systemdaten upsert _, err = tx.Exec(` INSERT INTO system_data (agent_id, data_json, updated_at) VALUES (?, ?, ?) ON CONFLICT(agent_id) DO UPDATE SET data_json=excluded.data_json, updated_at=excluded.updated_at `, agentID, string(jsonData), now) if err != nil { return err } return tx.Commit() } // GetAgents - Alle Agents auflisten func (d *Database) GetAgents() ([]models.Agent, error) { rows, err := d.db.Query("SELECT id, name, hostname, ip, opnsense_version, registered_at, last_heartbeat FROM agents ORDER BY name") if err != nil { return nil, err } defer rows.Close() var agents []models.Agent for rows.Next() { var a models.Agent var regAt string var lastHB sql.NullString if err := rows.Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, ®At, &lastHB); err != nil { return nil, err } a.RegisteredAt, _ = time.Parse(time.RFC3339, regAt) if lastHB.Valid { t, _ := time.Parse(time.RFC3339, lastHB.String) a.LastHeartbeat = &t } agents = append(agents, a) } return agents, rows.Err() } // GetAgent - Einzelnen Agent mit Systemdaten func (d *Database) GetAgentByHostname(hostname string) (*models.Agent, error) { var a models.Agent var regAt string var lastHB sql.NullString err := d.db.QueryRow("SELECT id, name, hostname, ip, opnsense_version, registered_at, last_heartbeat FROM agents WHERE hostname = ? LIMIT 1", hostname). Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, ®At, &lastHB) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } a.RegisteredAt, _ = time.Parse(time.RFC3339, regAt) if lastHB.Valid { t, _ := time.Parse(time.RFC3339, lastHB.String) a.LastHeartbeat = &t } return &a, nil } func (d *Database) GetAgent(id string) (*models.Agent, *models.SystemData, error) { var a models.Agent var regAt string var lastHB sql.NullString err := d.db.QueryRow("SELECT id, name, hostname, ip, opnsense_version, registered_at, last_heartbeat FROM agents WHERE id = ?", id). Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, ®At, &lastHB) if err == sql.ErrNoRows { return nil, nil, nil } if err != nil { return nil, nil, err } a.RegisteredAt, _ = time.Parse(time.RFC3339, regAt) if lastHB.Valid { t, _ := time.Parse(time.RFC3339, lastHB.String) a.LastHeartbeat = &t } // Systemdaten laden var dataJSON sql.NullString err = d.db.QueryRow("SELECT data_json FROM system_data WHERE agent_id = ?", id).Scan(&dataJSON) if err != nil && err != sql.ErrNoRows { return nil, nil, err } var sysData *models.SystemData if dataJSON.Valid { sysData = &models.SystemData{} if err := json.Unmarshal([]byte(dataJSON.String), sysData); err != nil { return &a, nil, nil } } return &a, sysData, nil } // GetSystemData - Nur Systemdaten eines Agents func (d *Database) GetSystemData(agentID string) (*models.SystemData, error) { var dataJSON string err := d.db.QueryRow("SELECT data_json FROM system_data WHERE agent_id = ?", agentID).Scan(&dataJSON) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } var data models.SystemData if err := json.Unmarshal([]byte(dataJSON), &data); err != nil { return nil, err } return &data, nil } // --- Config Backup Methoden --- // SaveConfigBackup speichert ein Config-Backup (dedupliziert nach Hash) func (d *Database) SaveConfigBackup(agentID, hash string, size int, configXML string) (int64, bool, error) { // Pruefen ob ein Backup mit gleichem Hash schon existiert var existingID int64 err := d.db.QueryRow("SELECT id FROM config_backups WHERE agent_id = ? AND hash = ? LIMIT 1", agentID, hash).Scan(&existingID) if err == nil { // Gleicher Hash existiert bereits — kein neues Backup noetig return existingID, false, nil } now := time.Now().UTC().Format(time.RFC3339) res, err := d.db.Exec( "INSERT INTO config_backups (agent_id, hash, size, config_xml, created_at) VALUES (?, ?, ?, ?, ?)", agentID, hash, size, configXML, now, ) if err != nil { return 0, false, fmt.Errorf("Backup speichern: %w", err) } id, _ := res.LastInsertId() return id, true, nil } // GetConfigBackups listet alle Backups eines Agents func (d *Database) GetConfigBackups(agentID string, limit int) ([]map[string]interface{}, error) { if limit <= 0 { limit = 50 } rows, err := d.db.Query( "SELECT id, hash, size, created_at FROM config_backups WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?", agentID, limit, ) if err != nil { return nil, err } defer rows.Close() var backups []map[string]interface{} for rows.Next() { var id int64 var hash, createdAt string var size int if err := rows.Scan(&id, &hash, &size, &createdAt); err != nil { return nil, err } backups = append(backups, map[string]interface{}{ "id": id, "hash": hash, "size": size, "created_at": createdAt, }) } if backups == nil { backups = []map[string]interface{}{} } return backups, rows.Err() } // GetConfigBackup holt ein einzelnes Backup inkl. config_xml func (d *Database) GetConfigBackup(agentID string, backupID int64) (map[string]interface{}, error) { var hash, configXML, createdAt string var size int var id int64 err := d.db.QueryRow( "SELECT id, hash, size, config_xml, created_at FROM config_backups WHERE agent_id = ? AND id = ?", agentID, backupID, ).Scan(&id, &hash, &size, &configXML, &createdAt) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } return map[string]interface{}{ "id": id, "hash": hash, "size": size, "config_xml": configXML, "created_at": createdAt, }, nil } // GetLatestConfigBackup holt das neueste Backup func (d *Database) GetLatestConfigBackup(agentID string) (map[string]interface{}, error) { var hash, configXML, createdAt string var size int var id int64 err := d.db.QueryRow( "SELECT id, hash, size, config_xml, created_at FROM config_backups WHERE agent_id = ? ORDER BY created_at DESC LIMIT 1", agentID, ).Scan(&id, &hash, &size, &configXML, &createdAt) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } return map[string]interface{}{ "id": id, "hash": hash, "size": size, "config_xml": configXML, "created_at": createdAt, }, nil } // DeleteConfigBackup loescht ein einzelnes Backup func (d *Database) DeleteConfigBackup(agentID string, backupID int64) (bool, error) { res, err := d.db.Exec("DELETE FROM config_backups WHERE agent_id = ? AND id = ?", agentID, backupID) if err != nil { return false, err } rows, _ := res.RowsAffected() return rows > 0, nil } // DeleteAgent - Agent und zugehoerige Daten loeschen func (d *Database) DeleteAgent(id string) (bool, error) { // Foreign Key Cascade aktivieren d.db.Exec("PRAGMA foreign_keys = ON") res, err := d.db.Exec("DELETE FROM agents WHERE id = ?", id) if err != nil { return false, err } rows, _ := res.RowsAffected() // Sicherheitshalber auch system_data loeschen (falls kein CASCADE) d.db.Exec("DELETE FROM system_data WHERE agent_id = ?", id) return rows > 0, nil }