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;
}
}