Agent-Updater, Multi-Plattform Firmware, Frontend Firmware-Seite, Agent-Tab, Zertifikate-Redesign

Updater:
- Separater Go-Daemon (rmm-updater) als eigener rc.d Service
- Pollt alle 60s: Firmware vorhanden + Hash unterschiedlich + Flag gesetzt
- Download -> SHA256 Verify -> Agent Stop -> Replace -> Agent Start
- Automatisches Rollback bei Fehler
- Deployed und getestet auf allen 3 Firewalls (.1, .19, .33)

Backend:
- Firmware-DB multi-plattform (freebsd/linux/windows)
- update_requested Flag pro Agent in agents Tabelle
- Heartbeat-Response liefert update_available wenn Flag gesetzt
- Neue Endpoints: request-update, request-update-all, firmware/download
- Auth: JWT + CombinedAuth Middleware (API-Key OR JWT)
- Customers CRUD + Agent-Zuordnung
- Users CRUD + Passwort-Aenderung

Frontend:
- Firmware-Seite: Multi-Plattform Upload, Agent-Liste mit Update-Trigger
- Agent-Tab im AgentPanel: Update-Button, nuetzliche Befehle, Agent-Info
- Zertifikate-Tab redesigned: Karten-Layout mit Restlaufzeit und Farbbalken
- Sidebar: Firmware-Eintrag hinzugefuegt

Plugin:
- install.sh erweitert: Installiert Agent + Updater + rc.d Services
- rmm_updater rc.d Service

README:
- Architektur-Diagramm erweitert (Frontend + Updater)
- Auth, Customers, Users, Firmware API dokumentiert
- Updater-Flow dokumentiert
- Frontend-Seiten und Deploy beschrieben
- DB-Schema aktualisiert (neue Tabellen)
- Dateistruktur erweitert (updater/, frontend/, firmware.go, auth.go)
This commit is contained in:
cynfo3000
2026-02-28 22:43:03 +01:00
parent 616a0cb6ac
commit e74cdce0ba
26 changed files with 2857 additions and 241 deletions
+121 -8
View File
@@ -67,6 +67,7 @@ func (d *Database) migrate() error {
hostname TEXT NOT NULL,
ip TEXT NOT NULL,
opnsense_version TEXT NOT NULL DEFAULT '',
agent_version TEXT NOT NULL DEFAULT '',
registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_heartbeat TIMESTAMPTZ
);
@@ -124,6 +125,27 @@ func (d *Database) migrate() error {
d.db.Exec("ALTER TABLE agents ADD COLUMN IF NOT EXISTS customer_id INTEGER REFERENCES customers(id)")
d.db.Exec("CREATE INDEX IF NOT EXISTS idx_agents_customer ON agents(customer_id)")
// Agents um agent_version und update_requested erweitern
d.db.Exec("ALTER TABLE agents ADD COLUMN IF NOT EXISTS agent_version TEXT NOT NULL DEFAULT ''")
d.db.Exec("ALTER TABLE agents ADD COLUMN IF NOT EXISTS update_requested BOOLEAN NOT NULL DEFAULT FALSE")
// Agent-Firmware Tabelle (Multi-Plattform)
d.db.Exec(`CREATE TABLE IF NOT EXISTS agent_firmware (
version TEXT PRIMARY KEY,
hash TEXT NOT NULL,
binary_data BYTEA NOT NULL,
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`)
// Platform-Spalte hinzufuegen (default freebsd fuer bestehende Eintraege)
d.db.Exec("ALTER TABLE agent_firmware DROP CONSTRAINT IF EXISTS agent_firmware_pkey")
d.db.Exec("ALTER TABLE agent_firmware ADD COLUMN IF NOT EXISTS platform TEXT NOT NULL DEFAULT 'freebsd'")
// Neuer Primary Key: version + platform
d.db.Exec(`DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'agent_firmware_version_platform_key') THEN
ALTER TABLE agent_firmware ADD CONSTRAINT agent_firmware_version_platform_key UNIQUE (version, platform);
END IF;
END $$`)
// Metrics Hypertable
metricsSchema := `
CREATE TABLE IF NOT EXISTS metrics (
@@ -220,14 +242,15 @@ func (d *Database) Close() error {
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)
INSERT INTO agents (id, name, hostname, ip, opnsense_version, agent_version, registered_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
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())
opnsense_version=EXCLUDED.opnsense_version,
agent_version=EXCLUDED.agent_version
`, agent.ID, agent.Name, agent.Hostname, agent.IP, agent.OPNsenseVersion, agent.AgentVersion, agent.RegisteredAt.UTC())
return err
}
@@ -342,7 +365,7 @@ func (d *Database) writeMetrics(tx *sql.Tx, agentID string, ts time.Time, data *
}
func (d *Database) GetAgents() ([]models.Agent, error) {
rows, err := d.db.Query("SELECT id, name, hostname, ip, opnsense_version, registered_at, last_heartbeat, customer_id FROM agents ORDER BY name")
rows, err := d.db.Query("SELECT id, name, hostname, ip, opnsense_version, agent_version, registered_at, last_heartbeat, customer_id, update_requested FROM agents ORDER BY name")
if err != nil {
return nil, err
}
@@ -354,7 +377,7 @@ func (d *Database) GetAgents() ([]models.Agent, error) {
var lastHB sql.NullTime
var custID sql.NullInt64
if err := rows.Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &a.RegisteredAt, &lastHB, &custID); err != nil {
if err := rows.Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &a.AgentVersion, &a.RegisteredAt, &lastHB, &custID, &a.UpdateRequested); err != nil {
return nil, err
}
if lastHB.Valid {
@@ -399,9 +422,9 @@ func (d *Database) GetAgent(id string) (*models.Agent, *models.SystemData, error
var custID sql.NullInt64
err := d.db.QueryRow(
"SELECT id, name, hostname, ip, opnsense_version, registered_at, last_heartbeat, customer_id FROM agents WHERE id = $1",
"SELECT id, name, hostname, ip, opnsense_version, agent_version, registered_at, last_heartbeat, customer_id, update_requested FROM agents WHERE id = $1",
id,
).Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &a.RegisteredAt, &lastHB, &custID)
).Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &a.AgentVersion, &a.RegisteredAt, &lastHB, &custID, &a.UpdateRequested)
if err == sql.ErrNoRows {
return nil, nil, nil
@@ -452,6 +475,96 @@ func (d *Database) GetSystemData(agentID string) (*models.SystemData, error) {
return &data, nil
}
func (d *Database) UpdateAgentVersion(id, version string) error {
_, err := d.db.Exec("UPDATE agents SET agent_version = $1 WHERE id = $2", version, id)
return err
}
// Update-Request Flag setzen/lesen/loeschen
func (d *Database) SetUpdateRequest(id string, requested bool) error {
_, err := d.db.Exec("UPDATE agents SET update_requested = $1 WHERE id = $2", requested, id)
return err
}
func (d *Database) IsUpdateRequested(id string) bool {
var requested bool
err := d.db.QueryRow("SELECT update_requested FROM agents WHERE id = $1", id).Scan(&requested)
return err == nil && requested
}
func (d *Database) ClearUpdateRequest(id string) error {
return d.SetUpdateRequest(id, false)
}
// Alle Agents mit Update-Flag setzen
func (d *Database) SetUpdateRequestAll(requested bool) (int64, error) {
res, err := d.db.Exec("UPDATE agents SET update_requested = $1", requested)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
// Agent-Firmware speichern (pro Plattform)
func (d *Database) SaveAgentFirmware(version, platform, hash string, binary []byte) error {
_, err := d.db.Exec(`
INSERT INTO agent_firmware (version, platform, hash, binary_data, uploaded_at)
VALUES ($1, $2, $3, $4, NOW())
ON CONFLICT(version, platform) DO UPDATE SET hash=EXCLUDED.hash, binary_data=EXCLUDED.binary_data, uploaded_at=NOW()
`, version, platform, hash, binary)
return err
}
func (d *Database) GetLatestFirmware(platform string) (string, string, []byte, error) {
var version, hash string
var binary []byte
err := d.db.QueryRow("SELECT version, hash, binary_data FROM agent_firmware WHERE platform = $1 ORDER BY uploaded_at DESC LIMIT 1", platform).Scan(&version, &hash, &binary)
if err != nil {
return "", "", nil, err
}
return version, hash, binary, nil
}
func (d *Database) GetFirmwareInfo(platform string) (string, string, int, error) {
var version, hash string
var size int
err := d.db.QueryRow("SELECT version, hash, length(binary_data) FROM agent_firmware WHERE platform = $1 ORDER BY uploaded_at DESC LIMIT 1", platform).Scan(&version, &hash, &size)
if err != nil {
return "", "", 0, err
}
return version, hash, size, nil
}
// Alle Plattform-Firmwares auflisten
func (d *Database) GetAllFirmwareInfo() ([]map[string]interface{}, error) {
rows, err := d.db.Query(`
SELECT DISTINCT ON (platform) platform, version, hash, length(binary_data) as size, uploaded_at
FROM agent_firmware ORDER BY platform, uploaded_at DESC
`)
if err != nil {
return nil, err
}
defer rows.Close()
var results []map[string]interface{}
for rows.Next() {
var platform, version, hash string
var size int
var uploadedAt time.Time
if err := rows.Scan(&platform, &version, &hash, &size, &uploadedAt); err != nil {
return nil, err
}
results = append(results, map[string]interface{}{
"platform": platform,
"version": version,
"hash": hash,
"size": size,
"uploaded_at": uploadedAt,
})
}
return results, nil
}
func (d *Database) DeleteAgent(id string) (bool, error) {
res, err := d.db.Exec("DELETE FROM agents WHERE id = $1", id)
if err != nil {