From c114df215a2598938dbf760eab1a50f0c134d500 Mon Sep 17 00:00:00 2001 From: cynfo3000 Date: Sun, 1 Mar 2026 01:37:35 +0100 Subject: [PATCH] Fix: SettingsController in install.sh aktualisiert (rmmagent[general] Parsing) --- agent/collector/opnsense.go | 68 ++++++++ agent/collector/services.go | 24 ++- agent/main.go | 2 + backend/api/handlers.go | 78 ++++++++- backend/db/postgres.go | 28 +++ backend/models/system.go | 9 + frontend/src/api/client.js | 6 + frontend/src/components/AgentPanel.jsx | 103 +++++++---- frontend/src/pages/Agents.jsx | 165 +++++++++++++++++- opnsense-plugin/install.sh | 68 +++++++- .../RMMAgent/Api/SettingsController.php | 43 +++-- 11 files changed, 534 insertions(+), 60 deletions(-) diff --git a/agent/collector/opnsense.go b/agent/collector/opnsense.go index 4c4dca8..ad879d1 100644 --- a/agent/collector/opnsense.go +++ b/agent/collector/opnsense.go @@ -1,6 +1,8 @@ package collector import ( + "encoding/json" + "os" "os/exec" "strconv" "strings" @@ -59,3 +61,69 @@ func CollectUptime() int64 { } return uptime } + +// LicenseInfo fuer OPNsense Business Edition +type LicenseInfo struct { + IsBusinessEdition bool `json:"is_business_edition"` + ProductName string `json:"product_name,omitempty"` + Subscription string `json:"subscription,omitempty"` + ValidTo string `json:"valid_to,omitempty"` + DaysRemaining int `json:"days_remaining,omitempty"` +} + +func CollectLicense() *LicenseInfo { + // core version file lesen + coreData, err := os.ReadFile("/usr/local/opnsense/version/core") + if err != nil { + return nil + } + + var core map[string]interface{} + if err := json.Unmarshal(coreData, &core); err != nil { + return nil + } + + productID, _ := core["product_id"].(string) + coreName, _ := core["CORE_NAME"].(string) + + isBusiness := productID == "opnsense-business" || coreName == "opnsense-business" + if !isBusiness { + return &LicenseInfo{IsBusinessEdition: false, ProductName: "OPNsense Community"} + } + + info := &LicenseInfo{ + IsBusinessEdition: true, + ProductName: "OPNsense Business Edition", + } + + // Lizenz-Ablauf lesen + licData, err := os.ReadFile("/usr/local/opnsense/version/core.license") + if err == nil { + var lic map[string]string + if json.Unmarshal(licData, &lic) == nil { + if validTo, ok := lic["valid_to"]; ok { + info.ValidTo = validTo + if t, err := time.Parse("2006-01-02", validTo); err == nil { + days := int(time.Until(t).Hours() / 24) + info.DaysRemaining = days + } + } + } + } + + // Subscription-Code aus config.xml lesen + out, err := exec.Command("/usr/local/bin/php", "-r", ` + $c = simplexml_load_file("/conf/config.xml"); + if (isset($c->system->firmware->subscription)) { + echo (string)$c->system->firmware->subscription; + } + `).Output() + if err == nil { + sub := strings.TrimSpace(string(out)) + if sub != "" { + info.Subscription = sub + } + } + + return info +} diff --git a/agent/collector/services.go b/agent/collector/services.go index a828d26..a27d867 100644 --- a/agent/collector/services.go +++ b/agent/collector/services.go @@ -23,8 +23,28 @@ func CollectServices() []ServiceInfo { return tryServiceList() } +// FreeBSD boot/init scripts that run once and are not real services +var ignoredServices = map[string]bool{ + "bgfsck": true, "cleanvar": true, "cleartmp": true, "devmatch": true, + "dmesg": true, "gptboot": true, "hostid": true, "hostid_save": true, + "kldxref": true, "mixer": true, "motd": true, "netif": true, + "newsyslog": true, "os-release": true, "rctl": true, "resolv": true, + "savecore": true, "utx": true, "var_run": true, "virecover": true, + "adjkerntz": true, "automount": true, "automountd": true, "autounmountd": true, + "cron": true, "devd": true, "dhclient": true, "fsck": true, + "growfs": true, "iovctl": true, "ip6addrctl": true, "ipsec": true, + "kld": true, "ldconfig": true, "linux": true, "local": true, + "localpkg": true, "lockd": true, "mdconfig": true, "mdconfig2": true, + "mountcritlocal": true, "mountcritremote": true, "mountlate": true, + "msgs": true, "nsswitch": true, "ntpdate": true, "pf": true, + "pflog": true, "powerd": true, "random": true, "root": true, + "routing": true, "securelevel": true, "serial": true, "statd": true, + "swap": true, "swaplate": true, "syscons": true, "sysctl": true, + "tmp": true, "ugidfw": true, "zfs": true, "zvol": true, +} + func tryPluginctl() []ServiceInfo { - out, err := exec.Command("pluginctl", "-s").Output() + out, err := exec.Command("/usr/local/sbin/pluginctl", "-s").Output() if err != nil { return nil } @@ -32,7 +52,7 @@ func tryPluginctl() []ServiceInfo { var services []ServiceInfo for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { name := strings.TrimSpace(line) - if name == "" { + if name == "" || ignoredServices[name] { continue } diff --git a/agent/main.go b/agent/main.go index b217694..015ed76 100644 --- a/agent/main.go +++ b/agent/main.go @@ -125,6 +125,7 @@ func sendHeartbeat(c *client.Client, agentID string) error { plugins := collector.CollectPlugins() updates := collector.CollectUpdates() cronJobs := collector.CollectCron() + license := collector.CollectLicense() req := map[string]interface{}{ "agent_id": agentID, @@ -164,6 +165,7 @@ func sendHeartbeat(c *client.Client, agentID string) error { "plugins": plugins, "updates": updates, "cron_jobs": cronJobs, + "license": license, }, } diff --git a/backend/api/handlers.go b/backend/api/handlers.go index 6d171b8..0e4a115 100644 --- a/backend/api/handlers.go +++ b/backend/api/handlers.go @@ -28,6 +28,7 @@ func (h *Handler) SetupRoutes(mux *http.ServeMux, hub *ws.Hub) { // Bestehende Agent-Routes mux.HandleFunc("POST /api/v1/agent/register", h.registerAgent) mux.HandleFunc("POST /api/v1/agent/heartbeat", h.heartbeat) + mux.HandleFunc("POST /api/v1/agents", h.preRegisterAgent) mux.HandleFunc("GET /api/v1/agents", h.listAgents) mux.HandleFunc("GET /api/v1/agents/events", h.getAllAgentEvents) mux.HandleFunc("GET /api/v1/agents/{id}", h.getAgent) @@ -114,6 +115,17 @@ func (h *Handler) registerAgent(w http.ResponseWriter, r *http.Request) { } } + if agentID == "" { + // Nach pre-registriertem Agent mit gleichem Namen suchen (hostname="(ausstehend)") + if existing, err := h.db.GetAgentByName(req.Name); err == nil && existing != nil { + if existing.Hostname == "(ausstehend)" { + agentID = existing.ID + isExisting = true + log.Printf("Pre-registrierten Agent gefunden: %s (%s)", existing.Name, existing.ID) + } + } + } + if agentID == "" { agentID = generateID() } @@ -145,6 +157,50 @@ func (h *Handler) registerAgent(w http.ResponseWriter, r *http.Request) { }) } +// POST /api/v1/agents - Pre-Registration einer Firewall +func (h *Handler) preRegisterAgent(w http.ResponseWriter, r *http.Request) { + var req struct { + Name string `json:"name"` + CustomerID *int `json:"customer_id,omitempty"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "Ungueltige Anfrage") + return + } + + if req.Name == "" { + writeError(w, http.StatusBadRequest, "Name ist erforderlich") + return + } + + agentID := generateID() + agent := &models.Agent{ + ID: agentID, + Name: req.Name, + Hostname: "(ausstehend)", + IP: "", + RegisteredAt: time.Now().UTC(), + } + + if err := h.db.RegisterAgent(agent); err != nil { + log.Printf("Fehler bei Pre-Registration: %v", err) + writeError(w, http.StatusInternalServerError, "Pre-Registration fehlgeschlagen") + return + } + + // Optionale Kundenzuordnung + if req.CustomerID != nil { + h.db.AssignAgentCustomer(agentID, *req.CustomerID) + } + + log.Printf("Agent pre-registriert: %s (%s)", req.Name, agentID) + writeJSON(w, http.StatusCreated, map[string]interface{}{ + "id": agentID, + "name": req.Name, + "message": "Firewall erfolgreich pre-registriert", + }) +} + // POST /api/v1/agent/heartbeat func (h *Handler) heartbeat(w http.ResponseWriter, r *http.Request) { var req models.HeartbeatRequest @@ -158,6 +214,9 @@ func (h *Handler) heartbeat(w http.ResponseWriter, r *http.Request) { return } + // Agent-Version VOR Update lesen (fuer Update-Flag Logik) + prevAgentVersion := h.db.GetAgentVersionFromDB(req.AgentID) + // Agent-Version aktualisieren if req.AgentVersion != "" { h.db.UpdateAgentVersion(req.AgentID, req.AgentVersion) @@ -177,14 +236,23 @@ func (h *Handler) heartbeat(w http.ResponseWriter, r *http.Request) { // Pruefen ob Update ansteht (default freebsd fuer OPNsense) if h.db.IsUpdateRequested(req.AgentID) { - if version, hash, _, err := h.db.GetFirmwareInfo("freebsd"); err == nil { - if version != req.AgentVersion { + version, hash, _, err := h.db.GetFirmwareInfo("freebsd") + if err != nil { + // Keine Firmware vorhanden — Flag sinnlos, loeschen + h.db.ClearUpdateRequest(req.AgentID) + } else if version == req.AgentVersion { + // Agent hat genau die Firmware-Version — fertig + h.db.ClearUpdateRequest(req.AgentID) + } else { + // Pruefen ob Agent-Version sich seit Flag-Setzung geaendert hat + // (z.B. manuelles Update auf andere Version) + if prevAgentVersion != "" && prevAgentVersion != req.AgentVersion { + // Version hat sich geaendert — Update war erfolgreich (manuell oder anderweitig) + h.db.ClearUpdateRequest(req.AgentID) + } else { resp.UpdateAvailable = true resp.UpdateVersion = version resp.UpdateHash = hash - } else { - // Gleiche Version — Flag zuruecksetzen - h.db.ClearUpdateRequest(req.AgentID) } } } diff --git a/backend/db/postgres.go b/backend/db/postgres.go index 9af285b..a15cd3d 100644 --- a/backend/db/postgres.go +++ b/backend/db/postgres.go @@ -416,6 +416,27 @@ func (d *Database) GetAgentByHostname(hostname string) (*models.Agent, error) { return &a, nil } +func (d *Database) GetAgentByName(name 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 name = $1 LIMIT 1", + name, + ).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 @@ -505,6 +526,13 @@ func (d *Database) SetUpdateRequestAll(requested bool) (int64, error) { return res.RowsAffected() } +// Agent-Version direkt aus DB lesen (vor dem Update durch Heartbeat) +func (d *Database) GetAgentVersionFromDB(id string) string { + var version string + d.db.QueryRow("SELECT agent_version FROM agents WHERE id = $1", id).Scan(&version) + return version +} + // Agent-Firmware speichern (pro Plattform) func (d *Database) SaveAgentFirmware(version, platform, hash string, binary []byte) error { _, err := d.db.Exec(` diff --git a/backend/models/system.go b/backend/models/system.go index 154c910..bcd29fe 100644 --- a/backend/models/system.go +++ b/backend/models/system.go @@ -21,6 +21,15 @@ type SystemData struct { Plugins []PluginInfo `json:"plugins,omitempty"` Updates *UpdateInfo `json:"updates,omitempty"` CronJobs []CronJob `json:"cron_jobs,omitempty"` + License *LicenseInfo `json:"license,omitempty"` +} + +type LicenseInfo struct { + IsBusinessEdition bool `json:"is_business_edition"` + ProductName string `json:"product_name,omitempty"` + Subscription string `json:"subscription,omitempty"` + ValidTo string `json:"valid_to,omitempty"` + DaysRemaining int `json:"days_remaining,omitempty"` } type WireGuardTunnel struct { diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index 247dcfe..482b210 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -97,6 +97,12 @@ class ApiClient { return this.del(`/api/v1/agents/${id}`) } + preRegisterAgent(name, customerId) { + const body = { name } + if (customerId) body.customer_id = customerId + return this.post('/api/v1/agents', body) + } + assignCustomer(agentId, customerId) { return this.put(`/api/v1/agents/${agentId}/customer`, { customer_id: customerId }) } diff --git a/frontend/src/components/AgentPanel.jsx b/frontend/src/components/AgentPanel.jsx index 86a1173..7f333da 100644 --- a/frontend/src/components/AgentPanel.jsx +++ b/frontend/src/components/AgentPanel.jsx @@ -60,9 +60,23 @@ export default function AgentPanel({ agentId, customers, onClose, onReload }) { {agent.hostname} · IP: {agent.ip} · {sys?.opnsense_version || agent.opnsense_version || '—'} - +
+ + +
{/* Tabs */} @@ -250,6 +264,47 @@ function OverviewTab({ sys }) { + {/* Lizenz */} + {sys.license && ( + <> +

Lizenz

+
+
+
+
{sys.license.product_name || 'OPNsense'}
+ {sys.license.subscription && ( +
{sys.license.subscription}
+ )} +
+ {sys.license.valid_to && ( +
+
+ {sys.license.days_remaining > 0 ? `${sys.license.days_remaining} Tage` : 'Abgelaufen'} +
+
gueltig bis {sys.license.valid_to}
+
+ )} +
+ {sys.license.valid_to && sys.license.days_remaining != null && ( +
+
+
+ )} +
+ + )} + {/* Gateways */} {sys.gateways && sys.gateways.length > 0 && ( <> @@ -1009,18 +1064,16 @@ function CertsTab({ sys }) { const remaining = (expires) => { if (!expires) return null const diff = expires - now - if (diff < 0) return { text: 'Abgelaufen', color: 'text-red-400', bg: 'bg-red-500/20 border-red-500/30', barColor: 'bg-red-500', pct: 0 } + if (diff < 0) return { text: 'Abgelaufen', color: 'text-red-400', bg: 'bg-red-500/10 border-red-500/20', barColor: 'bg-red-500', pct: 0 } const days = Math.floor(diff / 86400000) if (days > 365) { const y = Math.floor(days / 365) const m = Math.floor((days % 365) / 30) - return { text: `${y}J ${m}M`, color: 'text-emerald-400', bg: 'bg-emerald-500/20 border-emerald-500/30', barColor: 'bg-emerald-500', pct: 100 } + return { text: `${y}J ${m}M`, color: 'text-gray-400', bg: 'bg-gray-800/50 border-gray-700/50', barColor: 'bg-emerald-500', pct: 100 } } - if (days > 90) return { text: `${days} Tage`, color: 'text-emerald-400', bg: 'bg-emerald-500/20 border-emerald-500/30', barColor: 'bg-emerald-500', pct: Math.min(100, days / 3.65) } - if (days > 30) return { text: `${days} Tage`, color: 'text-yellow-400', bg: 'bg-yellow-500/20 border-yellow-500/30', barColor: 'bg-yellow-500', pct: Math.min(100, days / 3.65) } - if (days > 7) return { text: `${days} Tage`, color: 'text-orange-400', bg: 'bg-orange-500/20 border-orange-500/30', barColor: 'bg-orange-500', pct: Math.min(100, days / 3.65) } - if (days > 0) return { text: `${days} Tage`, color: 'text-red-400', bg: 'bg-red-500/20 border-red-500/30', barColor: 'bg-red-500', pct: Math.min(100, days / 3.65) } - return { text: 'Heute', color: 'text-red-400', bg: 'bg-red-500/20 border-red-500/30', barColor: 'bg-red-500', pct: 0 } + if (days > 7) return { text: `${days} Tage`, color: 'text-gray-400', bg: 'bg-gray-800/50 border-gray-700/50', barColor: 'bg-emerald-500', pct: Math.min(100, days / 3.65) } + if (days > 0) return { text: `${days} Tage`, color: 'text-yellow-400', bg: 'bg-yellow-500/10 border-yellow-500/20', barColor: 'bg-yellow-500', pct: Math.min(100, days / 3.65) } + return { text: 'Heute', color: 'text-red-400', bg: 'bg-red-500/10 border-red-500/20', barColor: 'bg-red-500', pct: 0 } } const sorted = [...certs].sort((a, b) => { @@ -1033,7 +1086,7 @@ function CertsTab({ sys }) { const soonCount = sorted.filter(c => { if (!c.not_after) return false const e = new Date(c.not_after) - return e >= now && (e - now) < 30 * 86400000 + return e >= now && (e - now) < 7 * 86400000 }).length return ( @@ -1053,7 +1106,7 @@ function CertsTab({ sys }) { {soonCount > 0 && (
{soonCount}
-
<30 Tage
+
<7 Tage
)}
@@ -1065,32 +1118,22 @@ function CertsTab({ sys }) { const r = remaining(expires) const type = c.type || (c.is_ca ? 'CA' : 'Cert') return ( -
-
-
-
- {c.name || c.subject || '—'} - {type} -
-
- {c.issuer || 'Unbekannter Aussteller'} -
+
+
+
+ {c.name || c.subject || '--'} + {type}
{r ? ( - <> -
{r.text}
-
- {expires ? `bis ${expires.toLocaleDateString('de-DE')}` : ''} -
- + {r.text} ) : ( -
Kein Ablauf
+ -- )}
{r && r.pct !== undefined && ( -
+
)} diff --git a/frontend/src/pages/Agents.jsx b/frontend/src/pages/Agents.jsx index 83f3e9c..33e3245 100644 --- a/frontend/src/pages/Agents.jsx +++ b/frontend/src/pages/Agents.jsx @@ -2,7 +2,7 @@ import { useEffect, useState, useMemo } from 'react' import api from '../api/client' import StatusBadge from '../components/StatusBadge' import AgentPanel from '../components/AgentPanel' -import { Search, ChevronUp, ChevronDown } from 'lucide-react' +import { Search, ChevronUp, ChevronDown, Plus, Copy, Check, X } from 'lucide-react' export default function Agents() { const [agents, setAgents] = useState([]) @@ -12,6 +12,8 @@ export default function Agents() { const [sortKey, setSortKey] = useState('customer') const [sortDir, setSortDir] = useState('asc') const [selectedId, setSelectedId] = useState(null) + const [showAddDialog, setShowAddDialog] = useState(false) + const [addResult, setAddResult] = useState(null) const reload = () => { Promise.all([api.getAgents(), api.getCustomers()]) @@ -137,6 +139,12 @@ export default function Agents() {

Firewalls ({agents.length})

+
@@ -234,6 +242,161 @@ export default function Agents() { onReload={reload} /> )} + + {/* Firewall hinzufuegen Dialog */} + {showAddDialog && ( + { + const res = await api.preRegisterAgent(name, custId) + setAddResult(res) + reload() + }} + onClose={() => { setShowAddDialog(false); setAddResult(null) }} + /> + )} +
+ ) +} + +function AddFirewallDialog({ customers, result, onAdd, onClose }) { + const [name, setName] = useState('') + const [custId, setCustId] = useState('') + const [error, setError] = useState('') + const [submitting, setSubmitting] = useState(false) + const [copied, setCopied] = useState(null) + + const BACKEND_URL = 'https://192.168.85.13:8443' + const API_KEY = '01532e5a7c9e70bf2757df77a2f5b9b9' + + const handleSubmit = async () => { + if (!name.trim()) { setError('Name ist erforderlich'); return } + setError('') + setSubmitting(true) + try { + await onAdd(name.trim(), custId ? parseInt(custId) : null) + } catch (e) { + setError(e.message) + } + setSubmitting(false) + } + + const copyText = (text, id) => { + navigator.clipboard.writeText(text) + setCopied(id) + setTimeout(() => setCopied(null), 2000) + } + + const CopyBtn = ({ text, id }) => ( + + ) + + const installCmd = `# Vom Mac Studio aus ausfuehren: + +# 1. Ins RMM-Verzeichnis wechseln: +cd /Users/cynfo/.openclaw/workspace/rmm + +# 2. Agent + Updater + Installer auf die Firewall kopieren: +scp build/rmm-agent build/rmm-updater root@:/tmp/ +scp opnsense-plugin/install.sh root@:/tmp/ + +# 3. Installation starten: +ssh root@ '/bin/sh /tmp/install.sh' + +# 4. Im OPNsense WebGUI unter Services > RMM Agent: +# Backend URL: ${BACKEND_URL} +# API Key: ${API_KEY} +# Agent Name: ${name || ''} +# -> Save & Apply` + + return ( +
+
e.stopPropagation()}> +
+

Firewall hinzufuegen

+ +
+ +
+ {!result ? ( + <> +
+ + setName(e.target.value)} + placeholder="z.B. OPN-KUNDE.intra.example.net" + className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-white placeholder-gray-600 focus:outline-none focus:border-orange-500" + autoFocus + onKeyDown={e => e.key === 'Enter' && handleSubmit()} + /> +
+
+ + +
+ {error &&

{error}

} + + + ) : ( + <> +
+ Firewall "{result.name}" erfolgreich angelegt (ID: {result.id?.substring(0, 12)}...) +
+ +
+

Installationsanleitung

+
+ {installCmd} + +
+
+ +
+
+ Backend URL: + {BACKEND_URL} + +
+
+ API Key: + {API_KEY} + +
+
+ +

+ Der Agent verbindet sich nach der Installation automatisch mit dem Backend. + Hostname und IP werden beim ersten Heartbeat aktualisiert. +

+ + )} +
+
) } diff --git a/opnsense-plugin/install.sh b/opnsense-plugin/install.sh index 3b5f3b5..34ee55b 100755 --- a/opnsense-plugin/install.sh +++ b/opnsense-plugin/install.sh @@ -273,9 +273,9 @@ class SettingsController extends ApiControllerBase public function getAction() { $mdl = new RMMAgent(); - $result = array('general' => array()); + $result = array('rmmagent' => array('general' => array())); foreach ($mdl->general->iterateItems() as $key => $node) { - $result['general'][$key] = (string)$node; + $result['rmmagent']['general'][$key] = (string)$node; } return $result; } @@ -285,20 +285,45 @@ class SettingsController extends ApiControllerBase $result = array('result' => 'failed'); if ($this->request->isPost()) { $mdl = new RMMAgent(); - $post = $this->request->getPost('general'); - if (!empty($post)) { - foreach ($post as $key => $value) { - $node = $mdl->general->$key; - if ($node !== null) { - $node->setValue($value); + $post = null; + + // OPNsense sends data as rmmagent[general][KEY] + $rmmagent = $this->request->getPost('rmmagent'); + if (!empty($rmmagent) && isset($rmmagent['general'])) { + $post = $rmmagent['general']; + } + + // Fallback: JSON body + if (empty($post)) { + $rawBody = $this->request->getRawBody(); + if (!empty($rawBody)) { + $jsonData = json_decode($rawBody, true); + if (isset($jsonData['rmmagent']['general'])) { + $post = $jsonData['rmmagent']['general']; + } elseif (isset($jsonData['general'])) { + $post = $jsonData['general']; } } } + + if (!empty($post)) { + foreach ($post as $key => $value) { + try { + $node = $mdl->general->$key; + if ($node !== null) { + $node->setValue($value); + } + } catch (\Exception $e) { + // skip + } + } + } + $valMsgs = $mdl->performValidation(); if ($valMsgs->count() > 0) { $result['validations'] = array(); foreach ($valMsgs as $msg) { - $result['validations']['general.' . $msg->getField()] = $msg->getMessage(); + $result['validations']['rmmagent.general.' . $msg->getField()] = $msg->getMessage(); } } else { $mdl->serializeToConfig(); @@ -575,6 +600,31 @@ function rmmagent_services() } INCEOF +# Initialen config.xml Eintrag erstellen (falls noch nicht vorhanden) +echo " Initialen config.xml Eintrag pruefen..." +cat > /tmp/rmm_init_config.php << 'INITEOF' +object(); +if (!isset($xml->OPNsense->rmmagent)) { + $rmm = $xml->OPNsense->addChild('rmmagent'); + $gen = $rmm->addChild('general'); + $gen->addChild('Enabled', '0'); + $gen->addChild('BackendUrl', ''); + $gen->addChild('ApiKey', ''); + $gen->addChild('AgentName', ''); + $gen->addChild('Interval', '60'); + $gen->addChild('Insecure', '1'); + $config->save(); + echo " Initialer rmmagent Eintrag in config.xml erstellt\n"; +} else { + echo " rmmagent Eintrag bereits vorhanden\n"; +} +INITEOF +php /tmp/rmm_init_config.php +rm -f /tmp/rmm_init_config.php + # Cache loeschen und configd neustarten rm -f /var/lib/php/cache/_usr_local_opnsense_mvc_app_views_opnsense_rmmagent_*.php service configd restart diff --git a/opnsense-plugin/src/opnsense/mvc/app/controllers/OPNsense/RMMAgent/Api/SettingsController.php b/opnsense-plugin/src/opnsense/mvc/app/controllers/OPNsense/RMMAgent/Api/SettingsController.php index 7a6502f..16184de 100644 --- a/opnsense-plugin/src/opnsense/mvc/app/controllers/OPNsense/RMMAgent/Api/SettingsController.php +++ b/opnsense-plugin/src/opnsense/mvc/app/controllers/OPNsense/RMMAgent/Api/SettingsController.php @@ -1,7 +1,5 @@ array()); - + $result = array('rmmagent' => array('general' => array())); foreach ($mdl->general->iterateItems() as $key => $node) { - $result['general'][$key] = (string)$node; + $result['rmmagent']['general'][$key] = (string)$node; } - return $result; } public function setAction() { $result = array('result' => 'failed'); - if ($this->request->isPost()) { $mdl = new RMMAgent(); + $post = null; + + // OPNsense sends data as rmmagent[general][KEY] + $rmmagent = $this->request->getPost('rmmagent'); + if (!empty($rmmagent) && isset($rmmagent['general'])) { + $post = $rmmagent['general']; + } + + // Fallback: JSON body + if (empty($post)) { + $rawBody = $this->request->getRawBody(); + if (!empty($rawBody)) { + $jsonData = json_decode($rawBody, true); + if (isset($jsonData['rmmagent']['general'])) { + $post = $jsonData['rmmagent']['general']; + } elseif (isset($jsonData['general'])) { + $post = $jsonData['general']; + } + } + } - $post = $this->request->getPost('general'); if (!empty($post)) { foreach ($post as $key => $value) { - $node = $mdl->general->$key; - if ($node !== null) { - $node->setValue($value); + try { + $node = $mdl->general->$key; + if ($node !== null) { + $node->setValue($value); + } + } catch (\Exception $e) { + // skip } } } @@ -41,7 +59,7 @@ class SettingsController extends ApiControllerBase if ($valMsgs->count() > 0) { $result['validations'] = array(); foreach ($valMsgs as $msg) { - $result['validations']['general.' . $msg->getField()] = $msg->getMessage(); + $result['validations']['rmmagent.general.' . $msg->getField()] = $msg->getMessage(); } } else { $mdl->serializeToConfig(); @@ -49,7 +67,6 @@ class SettingsController extends ApiControllerBase $result['result'] = 'saved'; } } - return $result; } }