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
This commit is contained in:
cynfo3000 2026-02-28 12:48:14 +01:00
parent 27d6492258
commit 5d7a3cbcc1
9 changed files with 822 additions and 429 deletions

View File

@ -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

83
backend/api/metrics.go Normal file
View File

@ -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)
}
}

View File

@ -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))

View File

@ -3,6 +3,8 @@ package config
import (
"os"
"github.com/cynfo/rmm-backend/db"
"gopkg.in/yaml.v3"
)
@ -11,7 +13,7 @@ type Config struct {
TLSCert string `yaml:"tls_cert"`
TLSKey string `yaml:"tls_key"`
APIKeys []string `yaml:"api_keys"`
DBPath string `yaml:"db_path"`
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

633
backend/db/postgres.go Normal file
View File

@ -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()
}

View File

@ -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, &regAt, &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, &regAt, &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, &regAt, &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
}

View File

@ -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
)

View File

@ -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=

View File

@ -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)
}