From 5d7a3cbcc110c968471dd18d05c33b0e45dfdcc9 Mon Sep 17 00:00:00 2001 From: cynfo3000 Date: Sat, 28 Feb 2026 12:48:14 +0100 Subject: [PATCH] SQLite durch PostgreSQL + TimescaleDB ersetzt - Backend DB komplett auf PostgreSQL 17 + TimescaleDB 2.25.1 umgestellt - pgx pure-Go Driver (kein CGO), modernc/sqlite entfernt - Hypertable 'metrics' fuer Time-Series Daten - Automatische Metrik-Extraktion bei jedem Heartbeat: CPU, RAM, Uptime, Disk, Network-Traffic, Gateway RTT/Loss - Retention Policy: 90 Tage, Compression nach 7 Tagen - Neue API-Endpoints: GET /metrics (raw) + GET /metrics/summary (aggregiert) - time_bucket() Aggregation (5min, 1h, etc.) - config.yaml: database-Block statt db_path - docs/POSTGRES_SETUP.md: Installationsanleitung - README aktualisiert --- README.md | 44 ++- backend/api/metrics.go | 83 +++++ backend/api/tunnel_proxy.go | 4 + backend/config/config.go | 28 +- backend/db/postgres.go | 633 ++++++++++++++++++++++++++++++++++++ backend/db/sqlite.go | 365 --------------------- backend/go.mod | 24 +- backend/go.sum | 68 ++-- backend/main.go | 2 +- 9 files changed, 822 insertions(+), 429 deletions(-) create mode 100644 backend/api/metrics.go create mode 100644 backend/db/postgres.go delete mode 100644 backend/db/sqlite.go diff --git a/README.md b/README.md index 5f91812..77dbec7 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,20 @@ daemon -f -p /var/run/rmm-agent.pid -o /usr/local/rmm/rmm-agent.log \ | POST | `/api/v1/agents/{id}/major-update` | Major-Upgrade auf Zielversion | | POST | `/api/v1/agents/{id}/reboot` | Reboot ausfuehren | +### Metriken (TimescaleDB) + +| Methode | Endpoint | Beschreibung | +|---------|----------|-------------| +| GET | `/api/v1/agents/{id}/metrics` | Rohe Metriken abfragen (`?metric=X&from=&to=&limit=N`) | +| GET | `/api/v1/agents/{id}/metrics/summary` | Aggregierte Metriken (`?metric=X&bucket=5+minutes&from=&to=`) | + +Metriken werden automatisch bei jedem Heartbeat geschrieben (alle 60s): +`cpu_usage`, `memory_used_percent`, `memory_used_bytes`, `memory_total_bytes`, +`uptime_seconds`, `disk_used_percent`, `disk_used_bytes`, `interface_rx_bytes`, +`interface_tx_bytes`, `gateway_rtt_ms`, `gateway_loss_percent` + +Retention: 90 Tage (Raw), Compression nach 7 Tagen. + ### Config-Backup | Methode | Endpoint | Beschreibung | @@ -337,6 +351,30 @@ curl -sk -H "X-API-Key: 01532e5a7c9e70bf2757df77a2f5b9b9" -X DELETE \ https://192.168.85.13:8443/api/v1/agents/6beb8dfd38ecf03eab6c0f21b4ac7bee/wireguard/peers/89c10e2d-148e-11f1-a7af-7c5a1c6c7b73 ``` +### Metriken-Beispiele + +```bash +# CPU-Auslastung der letzten 24h +curl -sk -H "X-API-Key: 01532e5a7c9e70bf2757df77a2f5b9b9" \ + "https://192.168.85.13:8443/api/v1/agents/6beb8dfd38ecf03eab6c0f21b4ac7bee/metrics?metric=cpu_usage" + +# RAM-Nutzung mit Zeitraum +curl -sk -H "X-API-Key: 01532e5a7c9e70bf2757df77a2f5b9b9" \ + "https://192.168.85.13:8443/api/v1/agents/6beb8dfd38ecf03eab6c0f21b4ac7bee/metrics?metric=memory_used_percent&from=2026-02-28T00:00:00Z&to=2026-02-28T23:59:59Z" + +# 5-Minuten-Durchschnitte der CPU +curl -sk -H "X-API-Key: 01532e5a7c9e70bf2757df77a2f5b9b9" \ + "https://192.168.85.13:8443/api/v1/agents/6beb8dfd38ecf03eab6c0f21b4ac7bee/metrics/summary?metric=cpu_usage&bucket=5+minutes" + +# Stuendliche RAM-Zusammenfassung +curl -sk -H "X-API-Key: 01532e5a7c9e70bf2757df77a2f5b9b9" \ + "https://192.168.85.13:8443/api/v1/agents/6beb8dfd38ecf03eab6c0f21b4ac7bee/metrics/summary?metric=memory_used_percent&bucket=1+hour" + +# Alle Metriken eines Agents (alle Typen, letzte 100) +curl -sk -H "X-API-Key: 01532e5a7c9e70bf2757df77a2f5b9b9" \ + "https://192.168.85.13:8443/api/v1/agents/6beb8dfd38ecf03eab6c0f21b4ac7bee/metrics?limit=100" +``` + ### Backup-Beispiele ```bash @@ -476,7 +514,7 @@ rmm/ │ ├── config/ │ │ └── config.go # YAML Config Loader │ ├── db/ -│ │ └── sqlite.go # SQLite (Agent + Systemdaten) +│ │ └── postgres.go # PostgreSQL + TimescaleDB (Agents, Backups, Metriken) │ ├── models/ │ │ ├── agent.go # Agent, Register/Heartbeat Structs │ │ └── system.go # SystemData, alle Collector-Structs @@ -485,6 +523,7 @@ rmm/ │ │ ├── tunnel_proxy.go # Tunnel + Exec + Update REST Endpoints │ │ ├── backup.go # Config-Backup Endpoints (trigger, list, download, diff) │ │ ├── wireguard.go # WireGuard Peer API Endpoints +│ │ ├── metrics.go # Metriken-Abfrage Endpoints (raw + summary) │ │ └── middleware.go # API-Key Auth, Logging │ └── ws/ │ ├── handler.go # WebSocket Upgrade, Connection R/W Pumps @@ -495,7 +534,8 @@ rmm/ ## Technische Details -- **CGO-frei**: `CGO_ENABLED=0`, pure-Go SQLite (modernc.org/sqlite) +- **PostgreSQL + TimescaleDB**: Agent-Registry, Config-Backups, Time-Series Metriken (pgx Driver) +- **CGO-frei**: Kein CGO noetig (pure-Go PostgreSQL Driver) - **Cross-Compilation**: Backend linux/amd64, Agent freebsd/amd64 - **TLS**: Self-signed Zertifikate (automatisch generiert) - **WebSocket**: gorilla/websocket, Binary Messages fuer Tunnel-Daten diff --git a/backend/api/metrics.go b/backend/api/metrics.go new file mode 100644 index 0000000..4536497 --- /dev/null +++ b/backend/api/metrics.go @@ -0,0 +1,83 @@ +package api + +import ( + "net/http" + "strconv" + "time" +) + +// GET /api/v1/agents/{id}/metrics — Metriken abfragen +func (h *Handler) getMetrics() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + agentID := r.PathValue("id") + metric := r.URL.Query().Get("metric") + + // Zeitraum (default: letzte 24h) + from := time.Now().Add(-24 * time.Hour) + to := time.Now() + + if f := r.URL.Query().Get("from"); f != "" { + if t, err := time.Parse(time.RFC3339, f); err == nil { + from = t + } + } + if t := r.URL.Query().Get("to"); t != "" { + if parsed, err := time.Parse(time.RFC3339, t); err == nil { + to = parsed + } + } + + limit := 1000 + if l := r.URL.Query().Get("limit"); l != "" { + if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { + limit = parsed + } + } + + results, err := h.db.GetMetrics(agentID, metric, from, to, limit) + if err != nil { + writeError(w, http.StatusInternalServerError, "Metriken laden fehlgeschlagen") + return + } + writeJSON(w, http.StatusOK, results) + } +} + +// GET /api/v1/agents/{id}/metrics/summary — Aggregierte Metriken (Durchschnitt/Min/Max) +func (h *Handler) getMetricsSummary() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + agentID := r.PathValue("id") + metric := r.URL.Query().Get("metric") + + if metric == "" { + writeError(w, http.StatusBadRequest, "Parameter 'metric' erforderlich") + return + } + + bucket := r.URL.Query().Get("bucket") + if bucket == "" { + bucket = "5 minutes" + } + + from := time.Now().Add(-24 * time.Hour) + to := time.Now() + + if f := r.URL.Query().Get("from"); f != "" { + if t, err := time.Parse(time.RFC3339, f); err == nil { + from = t + } + } + if t := r.URL.Query().Get("to"); t != "" { + if parsed, err := time.Parse(time.RFC3339, t); err == nil { + to = parsed + } + } + + results, err := h.db.GetMetricsSummary(agentID, metric, bucket, from, to) + if err != nil { + writeError(w, http.StatusInternalServerError, "Metriken laden fehlgeschlagen") + return + } + writeJSON(w, http.StatusOK, results) + } +} diff --git a/backend/api/tunnel_proxy.go b/backend/api/tunnel_proxy.go index 618fca9..7c9be99 100644 --- a/backend/api/tunnel_proxy.go +++ b/backend/api/tunnel_proxy.go @@ -26,6 +26,10 @@ func (h *Handler) setupTunnelRoutes(mux *http.ServeMux, hub *ws.Hub) { mux.HandleFunc("PUT /api/v1/agents/{id}/proxy/{tunnel_id}/", h.httpProxy(hub)) mux.HandleFunc("DELETE /api/v1/agents/{id}/proxy/{tunnel_id}/", h.httpProxy(hub)) + // Metrics-Endpoints + mux.HandleFunc("GET /api/v1/agents/{id}/metrics", h.getMetrics()) + mux.HandleFunc("GET /api/v1/agents/{id}/metrics/summary", h.getMetricsSummary()) + // WireGuard-Endpoints mux.HandleFunc("POST /api/v1/agents/{id}/wireguard/peers", h.wgAddPeer(hub)) mux.HandleFunc("GET /api/v1/agents/{id}/wireguard/peers", h.wgListPeers(hub)) diff --git a/backend/config/config.go b/backend/config/config.go index 0c05032..7112808 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -3,15 +3,17 @@ package config import ( "os" + "github.com/cynfo/rmm-backend/db" + "gopkg.in/yaml.v3" ) type Config struct { - ListenAddr string `yaml:"listen_addr"` - TLSCert string `yaml:"tls_cert"` - TLSKey string `yaml:"tls_key"` - APIKeys []string `yaml:"api_keys"` - DBPath string `yaml:"db_path"` + ListenAddr string `yaml:"listen_addr"` + TLSCert string `yaml:"tls_cert"` + TLSKey string `yaml:"tls_key"` + APIKeys []string `yaml:"api_keys"` + Database db.DBConfig `yaml:"database"` } func Load(path string) (*Config, error) { @@ -19,7 +21,14 @@ func Load(path string) (*Config, error) { ListenAddr: ":8443", TLSCert: "certs/server.crt", TLSKey: "certs/server.key", - DBPath: "rmm.db", + Database: db.DBConfig{ + Host: "127.0.0.1", + Port: 5432, + User: "rmm", + Password: "", + DBName: "rmm", + SSLMode: "disable", + }, } data, err := os.ReadFile(path) @@ -41,8 +50,11 @@ func Load(path string) (*Config, error) { if v := os.Getenv("RMM_TLS_KEY"); v != "" { cfg.TLSKey = v } - if v := os.Getenv("RMM_DB_PATH"); v != "" { - cfg.DBPath = v + if v := os.Getenv("RMM_DB_HOST"); v != "" { + cfg.Database.Host = v + } + if v := os.Getenv("RMM_DB_PASSWORD"); v != "" { + cfg.Database.Password = v } return cfg, nil diff --git a/backend/db/postgres.go b/backend/db/postgres.go new file mode 100644 index 0000000..d402979 --- /dev/null +++ b/backend/db/postgres.go @@ -0,0 +1,633 @@ +package db + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "log" + "strconv" + "strings" + "time" + + "github.com/cynfo/rmm-backend/models" + + _ "github.com/jackc/pgx/v5/stdlib" +) + +type Database struct { + db *sql.DB +} + +type DBConfig struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + User string `yaml:"user"` + Password string `yaml:"password"` + DBName string `yaml:"dbname"` + SSLMode string `yaml:"sslmode"` +} + +func New(cfg DBConfig) (*Database, error) { + dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", + cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode) + + db, err := sql.Open("pgx", dsn) + if err != nil { + return nil, fmt.Errorf("Datenbank oeffnen: %w", err) + } + + // Connection Pool konfigurieren + db.SetMaxOpenConns(25) + db.SetMaxIdleConns(5) + db.SetConnMaxLifetime(5 * time.Minute) + + // Verbindung testen + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := db.PingContext(ctx); err != nil { + return nil, fmt.Errorf("Datenbank-Verbindung fehlgeschlagen: %w", err) + } + + d := &Database{db: db} + if err := d.migrate(); err != nil { + return nil, fmt.Errorf("Migration fehlgeschlagen: %w", err) + } + + log.Printf("PostgreSQL verbunden: %s@%s:%d/%s", cfg.User, cfg.Host, cfg.Port, cfg.DBName) + return d, nil +} + +func (d *Database) migrate() error { + // Relationale Tabellen + 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 TIMESTAMPTZ NOT NULL DEFAULT NOW(), + last_heartbeat TIMESTAMPTZ + ); + + CREATE TABLE IF NOT EXISTS system_data ( + agent_id TEXT PRIMARY KEY REFERENCES agents(id) ON DELETE CASCADE, + data_json JSONB NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS config_backups ( + id SERIAL PRIMARY KEY, + agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE, + hash TEXT NOT NULL, + size INTEGER NOT NULL, + config_xml TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + + 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); + ` + + if _, err := d.db.Exec(schema); err != nil { + return fmt.Errorf("Schema anlegen: %w", err) + } + + // Metrics Hypertable + metricsSchema := ` + CREATE TABLE IF NOT EXISTS metrics ( + time TIMESTAMPTZ NOT NULL, + agent_id TEXT NOT NULL, + metric TEXT NOT NULL, + value DOUBLE PRECISION NOT NULL, + tags JSONB + ); + ` + if _, err := d.db.Exec(metricsSchema); err != nil { + return fmt.Errorf("Metrics-Tabelle anlegen: %w", err) + } + + // In Hypertable umwandeln (ignoriert wenn schon Hypertable) + var isHypertable bool + err := d.db.QueryRow(` + SELECT EXISTS ( + SELECT 1 FROM timescaledb_information.hypertables + WHERE hypertable_name = 'metrics' + ) + `).Scan(&isHypertable) + + if err != nil || !isHypertable { + _, err = d.db.Exec(`SELECT create_hypertable('metrics', 'time', if_not_exists => TRUE)`) + if err != nil { + log.Printf("Hypertable-Warnung (ggf. schon vorhanden): %v", err) + } else { + log.Println("Metrics Hypertable erstellt") + } + } + + // Indices fuer Metrics + d.db.Exec(`CREATE INDEX IF NOT EXISTS idx_metrics_agent_metric ON metrics(agent_id, metric, time DESC)`) + + // Retention Policy (90 Tage) + d.setupRetention() + + // Compression Policy + d.setupCompression() + + return nil +} + +func (d *Database) setupRetention() { + // Pruefen ob schon eine Policy existiert + var count int + d.db.QueryRow(` + SELECT COUNT(*) FROM timescaledb_information.jobs + WHERE hypertable_name = 'metrics' AND proc_name = 'policy_retention' + `).Scan(&count) + + if count == 0 { + _, err := d.db.Exec(`SELECT add_retention_policy('metrics', INTERVAL '90 days')`) + if err != nil { + log.Printf("Retention Policy Warnung: %v", err) + } else { + log.Println("Retention Policy: 90 Tage fuer metrics") + } + } +} + +func (d *Database) setupCompression() { + // Compression aktivieren + d.db.Exec(` + ALTER TABLE metrics SET ( + timescaledb.compress, + timescaledb.compress_segmentby = 'agent_id, metric', + timescaledb.compress_orderby = 'time DESC' + ) + `) + + var count int + d.db.QueryRow(` + SELECT COUNT(*) FROM timescaledb_information.jobs + WHERE hypertable_name = 'metrics' AND proc_name = 'policy_compression' + `).Scan(&count) + + if count == 0 { + _, err := d.db.Exec(`SELECT add_compression_policy('metrics', INTERVAL '7 days')`) + if err != nil { + log.Printf("Compression Policy Warnung: %v", err) + } else { + log.Println("Compression Policy: nach 7 Tagen fuer metrics") + } + } +} + +func (d *Database) Close() error { + return d.db.Close() +} + +// --- Agent Methoden --- + +func (d *Database) RegisterAgent(agent *models.Agent) error { + _, err := d.db.Exec(` + INSERT INTO agents (id, name, hostname, ip, opnsense_version, registered_at) + VALUES ($1, $2, $3, $4, $5, $6) + 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()) + return err +} + +func (d *Database) SaveSystemData(agentID string, data *models.SystemData) error { + now := time.Now().UTC() + + 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 aktualisieren + res, err := tx.Exec("UPDATE agents SET last_heartbeat = $1 WHERE id = $2", 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 ($1, $2, $3) + 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 + } + + // Metriken extrahieren und schreiben + if err := d.writeMetrics(tx, agentID, now, data); err != nil { + log.Printf("Metriken schreiben Warnung: %v", err) + // Kein Abbruch — Systemdaten sind wichtiger + } + + return tx.Commit() +} + +// writeMetrics extrahiert Metriken aus den Systemdaten und schreibt sie in die Hypertable +func (d *Database) writeMetrics(tx *sql.Tx, agentID string, ts time.Time, data *models.SystemData) error { + stmt, err := tx.Prepare(`INSERT INTO metrics (time, agent_id, metric, value, tags) VALUES ($1, $2, $3, $4, $5)`) + if err != nil { + return err + } + defer stmt.Close() + + // CPU + if data.CPU.UsagePercent > 0 { + stmt.Exec(ts, agentID, "cpu_usage", data.CPU.UsagePercent, nil) + } + + // Memory + if data.Memory.TotalBytes > 0 { + stmt.Exec(ts, agentID, "memory_total_bytes", float64(data.Memory.TotalBytes), nil) + stmt.Exec(ts, agentID, "memory_used_bytes", float64(data.Memory.UsedBytes), nil) + usedPercent := float64(data.Memory.UsedBytes) / float64(data.Memory.TotalBytes) * 100 + stmt.Exec(ts, agentID, "memory_used_percent", usedPercent, nil) + } + + // Uptime + if data.UptimeSeconds > 0 { + stmt.Exec(ts, agentID, "uptime_seconds", float64(data.UptimeSeconds), nil) + } + + // Disks + for _, disk := range data.Disks { + if disk.TotalBytes > 0 { + usedPercent := float64(disk.UsedBytes) / float64(disk.TotalBytes) * 100 + tags, _ := json.Marshal(map[string]string{"mountpoint": disk.MountPoint, "dataset": disk.Filesystem}) + stmt.Exec(ts, agentID, "disk_used_percent", usedPercent, string(tags)) + stmt.Exec(ts, agentID, "disk_used_bytes", float64(disk.UsedBytes), string(tags)) + } + } + + // Network Interfaces + for _, iface := range data.NetworkInterfaces { + if iface.RxBytes > 0 || iface.TxBytes > 0 { + tags, _ := json.Marshal(map[string]string{"interface": iface.Name}) + stmt.Exec(ts, agentID, "interface_rx_bytes", float64(iface.RxBytes), string(tags)) + stmt.Exec(ts, agentID, "interface_tx_bytes", float64(iface.TxBytes), string(tags)) + } + } + + // Gateways (Loss und Delay sind Strings wie "0.0%" und "1.2ms") + for _, gw := range data.Gateways { + if gw.Delay != "" { + tags, _ := json.Marshal(map[string]string{"gateway": gw.Name}) + // Delay parsen ("1.2ms" -> 1.2) + delayStr := strings.TrimSuffix(strings.TrimSpace(gw.Delay), "ms") + if delay, err := strconv.ParseFloat(delayStr, 64); err == nil { + stmt.Exec(ts, agentID, "gateway_rtt_ms", delay, string(tags)) + } + // Loss parsen ("0.0%" -> 0.0) + lossStr := strings.TrimSuffix(strings.TrimSpace(gw.Loss), "%") + if loss, err := strconv.ParseFloat(lossStr, 64); err == nil { + stmt.Exec(ts, agentID, "gateway_loss_percent", loss, string(tags)) + } + } + } + + return nil +} + +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 lastHB sql.NullTime + + if err := rows.Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &a.RegisteredAt, &lastHB); err != nil { + return nil, err + } + if lastHB.Valid { + a.LastHeartbeat = &lastHB.Time + } + agents = append(agents, a) + } + if agents == nil { + agents = []models.Agent{} + } + return agents, rows.Err() +} + +func (d *Database) GetAgentByHostname(hostname string) (*models.Agent, error) { + var a models.Agent + var lastHB sql.NullTime + + err := d.db.QueryRow( + "SELECT id, name, hostname, ip, opnsense_version, registered_at, last_heartbeat FROM agents WHERE hostname = $1 LIMIT 1", + hostname, + ).Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &a.RegisteredAt, &lastHB) + + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + if lastHB.Valid { + a.LastHeartbeat = &lastHB.Time + } + return &a, nil +} + +func (d *Database) GetAgent(id string) (*models.Agent, *models.SystemData, error) { + var a models.Agent + var lastHB sql.NullTime + + err := d.db.QueryRow( + "SELECT id, name, hostname, ip, opnsense_version, registered_at, last_heartbeat FROM agents WHERE id = $1", + id, + ).Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &a.RegisteredAt, &lastHB) + + if err == sql.ErrNoRows { + return nil, nil, nil + } + if err != nil { + return nil, nil, err + } + if lastHB.Valid { + a.LastHeartbeat = &lastHB.Time + } + + // Systemdaten laden + var dataJSON sql.NullString + err = d.db.QueryRow("SELECT data_json FROM system_data WHERE agent_id = $1", 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 +} + +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 = $1", 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 +} + +func (d *Database) DeleteAgent(id string) (bool, error) { + res, err := d.db.Exec("DELETE FROM agents WHERE id = $1", id) + if err != nil { + return false, err + } + rows, _ := res.RowsAffected() + return rows > 0, nil +} + +// --- Config Backup Methoden --- + +func (d *Database) SaveConfigBackup(agentID, hash string, size int, configXML string) (int64, bool, error) { + var existingID int64 + err := d.db.QueryRow("SELECT id FROM config_backups WHERE agent_id = $1 AND hash = $2 LIMIT 1", agentID, hash).Scan(&existingID) + if err == nil { + return existingID, false, nil + } + + var id int64 + err = d.db.QueryRow( + "INSERT INTO config_backups (agent_id, hash, size, config_xml) VALUES ($1, $2, $3, $4) RETURNING id", + agentID, hash, size, configXML, + ).Scan(&id) + if err != nil { + return 0, false, fmt.Errorf("Backup speichern: %w", err) + } + return id, true, nil +} + +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 = $1 ORDER BY created_at DESC LIMIT $2", + agentID, limit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var backups []map[string]interface{} + for rows.Next() { + var id int64 + var hash string + var size int + var createdAt time.Time + 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.Format(time.RFC3339), + }) + } + if backups == nil { + backups = []map[string]interface{}{} + } + return backups, rows.Err() +} + +func (d *Database) GetConfigBackup(agentID string, backupID int64) (map[string]interface{}, error) { + var hash, configXML string + var size int + var id int64 + var createdAt time.Time + + err := d.db.QueryRow( + "SELECT id, hash, size, config_xml, created_at FROM config_backups WHERE agent_id = $1 AND id = $2", + 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.Format(time.RFC3339), + }, nil +} + +func (d *Database) GetLatestConfigBackup(agentID string) (map[string]interface{}, error) { + var hash, configXML string + var size int + var id int64 + var createdAt time.Time + + err := d.db.QueryRow( + "SELECT id, hash, size, config_xml, created_at FROM config_backups WHERE agent_id = $1 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.Format(time.RFC3339), + }, nil +} + +func (d *Database) DeleteConfigBackup(agentID string, backupID int64) (bool, error) { + res, err := d.db.Exec("DELETE FROM config_backups WHERE agent_id = $1 AND id = $2", agentID, backupID) + if err != nil { + return false, err + } + rows, _ := res.RowsAffected() + return rows > 0, nil +} + +// --- Metrics Abfragen --- + +// GetMetrics holt Metriken fuer einen Agent in einem Zeitraum +func (d *Database) GetMetrics(agentID, metric string, from, to time.Time, limit int) ([]map[string]interface{}, error) { + if limit <= 0 { + limit = 1000 + } + + query := `SELECT time, metric, value, tags FROM metrics + WHERE agent_id = $1 AND time >= $2 AND time <= $3` + args := []interface{}{agentID, from, to} + + if metric != "" { + query += " AND metric = $4" + args = append(args, metric) + } + + query += " ORDER BY time DESC LIMIT $" + fmt.Sprintf("%d", len(args)+1) + args = append(args, limit) + + rows, err := d.db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var results []map[string]interface{} + for rows.Next() { + var ts time.Time + var m string + var v float64 + var tags sql.NullString + if err := rows.Scan(&ts, &m, &v, &tags); err != nil { + return nil, err + } + entry := map[string]interface{}{ + "time": ts.Format(time.RFC3339), + "metric": m, + "value": v, + } + if tags.Valid { + var t map[string]interface{} + json.Unmarshal([]byte(tags.String), &t) + entry["tags"] = t + } + results = append(results, entry) + } + if results == nil { + results = []map[string]interface{}{} + } + return results, rows.Err() +} + +// GetMetricsSummary holt aggregierte Metriken (avg/min/max) in Zeitbuckts +func (d *Database) GetMetricsSummary(agentID, metric, bucket string, from, to time.Time) ([]map[string]interface{}, error) { + if bucket == "" { + bucket = "5 minutes" + } + + rows, err := d.db.Query(` + SELECT time_bucket($1::interval, time) AS t, + AVG(value) AS avg_value, + MIN(value) AS min_value, + MAX(value) AS max_value, + COUNT(*) AS samples + FROM metrics + WHERE agent_id = $2 AND metric = $3 AND time >= $4 AND time <= $5 + GROUP BY t + ORDER BY t DESC + `, bucket, agentID, metric, from, to) + if err != nil { + return nil, err + } + defer rows.Close() + + var results []map[string]interface{} + for rows.Next() { + var t time.Time + var avg, min, max float64 + var samples int + if err := rows.Scan(&t, &avg, &min, &max, &samples); err != nil { + return nil, err + } + results = append(results, map[string]interface{}{ + "time": t.Format(time.RFC3339), + "avg": avg, + "min": min, + "max": max, + "samples": samples, + }) + } + if results == nil { + results = []map[string]interface{}{} + } + return results, rows.Err() +} diff --git a/backend/db/sqlite.go b/backend/db/sqlite.go deleted file mode 100644 index efa274c..0000000 --- a/backend/db/sqlite.go +++ /dev/null @@ -1,365 +0,0 @@ -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 -} diff --git a/backend/go.mod b/backend/go.mod index ae23baf..dd593b4 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,25 +1,19 @@ module github.com/cynfo/rmm-backend -go 1.22 +go 1.24.0 require ( github.com/gorilla/websocket v1.5.3 + github.com/jackc/pgx/v5 v5.8.0 gopkg.in/yaml.v3 v3.0.1 - modernc.org/sqlite v1.29.1 ) require ( - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/ncruces/go-strftime v0.1.9 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - golang.org/x/sys v0.16.0 // indirect - modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect - modernc.org/libc v1.41.0 // indirect - modernc.org/mathutil v1.6.0 // indirect - modernc.org/memory v1.7.2 // indirect - modernc.org/strutil v1.2.0 // indirect - modernc.org/token v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/text v0.29.0 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index aaef8af..d3cbaac 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,45 +1,37 @@ -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= -github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= -modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= -modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk= -modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= -modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= -modernc.org/sqlite v1.29.1 h1:19GY2qvWB4VPw0HppFlZCPAbmxFU41r+qjKZQdQ1ryA= -modernc.org/sqlite v1.29.1/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0= -modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= -modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/backend/main.go b/backend/main.go index 8107d42..5ad1284 100644 --- a/backend/main.go +++ b/backend/main.go @@ -39,7 +39,7 @@ func main() { } // Datenbank initialisieren - database, err := db.New(cfg.DBPath) + database, err := db.New(cfg.Database) if err != nil { log.Fatalf("Datenbank-Fehler: %v", err) }