Initial commit: RMM Agent + Backend
- Go Agent (FreeBSD/amd64) fuer OPNsense - Go Backend (Linux/amd64) mit REST API + SQLite - Collector: Hardware, CPU, Memory, Disks, Network, Services, WireGuard, DHCP (KEA/ISC/dnsmasq), Routes, Gateways, Certificates, Plugins, Updates - Persistente Agent-ID - TLS + API-Key Auth - WebSocket-Infrastruktur (WIP): bidirektionaler Command-Kanal, TCP-Tunnel fuer Remote-Zugriff auf Firewall-WebUI - Lastenheft und README
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
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 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user