Fix: SettingsController in install.sh aktualisiert (rmmagent[general] Parsing)
This commit is contained in:
parent
77ec200b42
commit
c114df215a
@ -1,6 +1,8 @@
|
|||||||
package collector
|
package collector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -59,3 +61,69 @@ func CollectUptime() int64 {
|
|||||||
}
|
}
|
||||||
return uptime
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -23,8 +23,28 @@ func CollectServices() []ServiceInfo {
|
|||||||
return tryServiceList()
|
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 {
|
func tryPluginctl() []ServiceInfo {
|
||||||
out, err := exec.Command("pluginctl", "-s").Output()
|
out, err := exec.Command("/usr/local/sbin/pluginctl", "-s").Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -32,7 +52,7 @@ func tryPluginctl() []ServiceInfo {
|
|||||||
var services []ServiceInfo
|
var services []ServiceInfo
|
||||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||||
name := strings.TrimSpace(line)
|
name := strings.TrimSpace(line)
|
||||||
if name == "" {
|
if name == "" || ignoredServices[name] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -125,6 +125,7 @@ func sendHeartbeat(c *client.Client, agentID string) error {
|
|||||||
plugins := collector.CollectPlugins()
|
plugins := collector.CollectPlugins()
|
||||||
updates := collector.CollectUpdates()
|
updates := collector.CollectUpdates()
|
||||||
cronJobs := collector.CollectCron()
|
cronJobs := collector.CollectCron()
|
||||||
|
license := collector.CollectLicense()
|
||||||
|
|
||||||
req := map[string]interface{}{
|
req := map[string]interface{}{
|
||||||
"agent_id": agentID,
|
"agent_id": agentID,
|
||||||
@ -164,6 +165,7 @@ func sendHeartbeat(c *client.Client, agentID string) error {
|
|||||||
"plugins": plugins,
|
"plugins": plugins,
|
||||||
"updates": updates,
|
"updates": updates,
|
||||||
"cron_jobs": cronJobs,
|
"cron_jobs": cronJobs,
|
||||||
|
"license": license,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,7 @@ func (h *Handler) SetupRoutes(mux *http.ServeMux, hub *ws.Hub) {
|
|||||||
// Bestehende Agent-Routes
|
// Bestehende Agent-Routes
|
||||||
mux.HandleFunc("POST /api/v1/agent/register", h.registerAgent)
|
mux.HandleFunc("POST /api/v1/agent/register", h.registerAgent)
|
||||||
mux.HandleFunc("POST /api/v1/agent/heartbeat", h.heartbeat)
|
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", h.listAgents)
|
||||||
mux.HandleFunc("GET /api/v1/agents/events", h.getAllAgentEvents)
|
mux.HandleFunc("GET /api/v1/agents/events", h.getAllAgentEvents)
|
||||||
mux.HandleFunc("GET /api/v1/agents/{id}", h.getAgent)
|
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 == "" {
|
if agentID == "" {
|
||||||
agentID = generateID()
|
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
|
// POST /api/v1/agent/heartbeat
|
||||||
func (h *Handler) heartbeat(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) heartbeat(w http.ResponseWriter, r *http.Request) {
|
||||||
var req models.HeartbeatRequest
|
var req models.HeartbeatRequest
|
||||||
@ -158,6 +214,9 @@ func (h *Handler) heartbeat(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Agent-Version VOR Update lesen (fuer Update-Flag Logik)
|
||||||
|
prevAgentVersion := h.db.GetAgentVersionFromDB(req.AgentID)
|
||||||
|
|
||||||
// Agent-Version aktualisieren
|
// Agent-Version aktualisieren
|
||||||
if req.AgentVersion != "" {
|
if req.AgentVersion != "" {
|
||||||
h.db.UpdateAgentVersion(req.AgentID, 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)
|
// Pruefen ob Update ansteht (default freebsd fuer OPNsense)
|
||||||
if h.db.IsUpdateRequested(req.AgentID) {
|
if h.db.IsUpdateRequested(req.AgentID) {
|
||||||
if version, hash, _, err := h.db.GetFirmwareInfo("freebsd"); err == nil {
|
version, hash, _, err := h.db.GetFirmwareInfo("freebsd")
|
||||||
if version != req.AgentVersion {
|
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.UpdateAvailable = true
|
||||||
resp.UpdateVersion = version
|
resp.UpdateVersion = version
|
||||||
resp.UpdateHash = hash
|
resp.UpdateHash = hash
|
||||||
} else {
|
|
||||||
// Gleiche Version — Flag zuruecksetzen
|
|
||||||
h.db.ClearUpdateRequest(req.AgentID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -416,6 +416,27 @@ func (d *Database) GetAgentByHostname(hostname string) (*models.Agent, error) {
|
|||||||
return &a, nil
|
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) {
|
func (d *Database) GetAgent(id string) (*models.Agent, *models.SystemData, error) {
|
||||||
var a models.Agent
|
var a models.Agent
|
||||||
var lastHB sql.NullTime
|
var lastHB sql.NullTime
|
||||||
@ -505,6 +526,13 @@ func (d *Database) SetUpdateRequestAll(requested bool) (int64, error) {
|
|||||||
return res.RowsAffected()
|
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)
|
// Agent-Firmware speichern (pro Plattform)
|
||||||
func (d *Database) SaveAgentFirmware(version, platform, hash string, binary []byte) error {
|
func (d *Database) SaveAgentFirmware(version, platform, hash string, binary []byte) error {
|
||||||
_, err := d.db.Exec(`
|
_, err := d.db.Exec(`
|
||||||
|
|||||||
@ -21,6 +21,15 @@ type SystemData struct {
|
|||||||
Plugins []PluginInfo `json:"plugins,omitempty"`
|
Plugins []PluginInfo `json:"plugins,omitempty"`
|
||||||
Updates *UpdateInfo `json:"updates,omitempty"`
|
Updates *UpdateInfo `json:"updates,omitempty"`
|
||||||
CronJobs []CronJob `json:"cron_jobs,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 {
|
type WireGuardTunnel struct {
|
||||||
|
|||||||
@ -97,6 +97,12 @@ class ApiClient {
|
|||||||
return this.del(`/api/v1/agents/${id}`)
|
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) {
|
assignCustomer(agentId, customerId) {
|
||||||
return this.put(`/api/v1/agents/${agentId}/customer`, { customer_id: customerId })
|
return this.put(`/api/v1/agents/${agentId}/customer`, { customer_id: customerId })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,9 +60,23 @@ export default function AgentPanel({ agentId, customers, onClose, onReload }) {
|
|||||||
{agent.hostname} · IP: {agent.ip} · {sys?.opnsense_version || agent.opnsense_version || '—'}
|
{agent.hostname} · IP: {agent.ip} · {sys?.opnsense_version || agent.opnsense_version || '—'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onClose} className="text-gray-400 hover:text-white">
|
<div className="flex items-center gap-2">
|
||||||
<X className="w-5 h-5" />
|
<button
|
||||||
</button>
|
onClick={async () => {
|
||||||
|
if (!confirm(`Firewall "${agent.name}" wirklich loeschen?`)) return
|
||||||
|
await api.deleteAgent(agent.id)
|
||||||
|
if (onReload) onReload()
|
||||||
|
onClose()
|
||||||
|
}}
|
||||||
|
className="text-gray-500 hover:text-red-400 transition-colors"
|
||||||
|
title="Firewall loeschen"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button onClick={onClose} className="text-gray-400 hover:text-white">
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
@ -250,6 +264,47 @@ function OverviewTab({ sys }) {
|
|||||||
<StatusCard value={`${svcRunning}/${svcTotal}`} label="Dienste" />
|
<StatusCard value={`${svcRunning}/${svcTotal}`} label="Dienste" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Lizenz */}
|
||||||
|
{sys.license && (
|
||||||
|
<>
|
||||||
|
<h3 className="text-sm font-medium text-gray-300 mt-4">Lizenz</h3>
|
||||||
|
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-white font-medium">{sys.license.product_name || 'OPNsense'}</div>
|
||||||
|
{sys.license.subscription && (
|
||||||
|
<div className="text-xs text-gray-500 mt-1 font-mono">{sys.license.subscription}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{sys.license.valid_to && (
|
||||||
|
<div className="text-right">
|
||||||
|
<div className={`text-lg font-bold ${
|
||||||
|
sys.license.days_remaining < 30 ? 'text-red-400' :
|
||||||
|
sys.license.days_remaining < 90 ? 'text-yellow-400' :
|
||||||
|
'text-green-400'
|
||||||
|
}`}>
|
||||||
|
{sys.license.days_remaining > 0 ? `${sys.license.days_remaining} Tage` : 'Abgelaufen'}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">gueltig bis {sys.license.valid_to}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{sys.license.valid_to && sys.license.days_remaining != null && (
|
||||||
|
<div className="mt-3 w-full h-2 bg-gray-700 rounded overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full rounded ${
|
||||||
|
sys.license.days_remaining < 30 ? 'bg-red-500' :
|
||||||
|
sys.license.days_remaining < 90 ? 'bg-yellow-500' :
|
||||||
|
'bg-green-500'
|
||||||
|
}`}
|
||||||
|
style={{ width: `${Math.min(100, Math.max(2, sys.license.days_remaining / 365 * 100))}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Gateways */}
|
{/* Gateways */}
|
||||||
{sys.gateways && sys.gateways.length > 0 && (
|
{sys.gateways && sys.gateways.length > 0 && (
|
||||||
<>
|
<>
|
||||||
@ -1009,18 +1064,16 @@ function CertsTab({ sys }) {
|
|||||||
const remaining = (expires) => {
|
const remaining = (expires) => {
|
||||||
if (!expires) return null
|
if (!expires) return null
|
||||||
const diff = expires - now
|
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)
|
const days = Math.floor(diff / 86400000)
|
||||||
if (days > 365) {
|
if (days > 365) {
|
||||||
const y = Math.floor(days / 365)
|
const y = Math.floor(days / 365)
|
||||||
const m = Math.floor((days % 365) / 30)
|
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 > 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 > 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 > 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) }
|
||||||
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) }
|
return { text: 'Heute', color: 'text-red-400', bg: 'bg-red-500/10 border-red-500/20', barColor: 'bg-red-500', pct: 0 }
|
||||||
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 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sorted = [...certs].sort((a, b) => {
|
const sorted = [...certs].sort((a, b) => {
|
||||||
@ -1033,7 +1086,7 @@ function CertsTab({ sys }) {
|
|||||||
const soonCount = sorted.filter(c => {
|
const soonCount = sorted.filter(c => {
|
||||||
if (!c.not_after) return false
|
if (!c.not_after) return false
|
||||||
const e = new Date(c.not_after)
|
const e = new Date(c.not_after)
|
||||||
return e >= now && (e - now) < 30 * 86400000
|
return e >= now && (e - now) < 7 * 86400000
|
||||||
}).length
|
}).length
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -1053,7 +1106,7 @@ function CertsTab({ sys }) {
|
|||||||
{soonCount > 0 && (
|
{soonCount > 0 && (
|
||||||
<div className="bg-yellow-500/10 rounded-lg border border-yellow-500/30 px-4 py-2 flex-1 text-center">
|
<div className="bg-yellow-500/10 rounded-lg border border-yellow-500/30 px-4 py-2 flex-1 text-center">
|
||||||
<div className="text-2xl font-bold text-yellow-400">{soonCount}</div>
|
<div className="text-2xl font-bold text-yellow-400">{soonCount}</div>
|
||||||
<div className="text-xs text-yellow-400/70"><30 Tage</div>
|
<div className="text-xs text-yellow-400/70"><7 Tage</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -1065,32 +1118,22 @@ function CertsTab({ sys }) {
|
|||||||
const r = remaining(expires)
|
const r = remaining(expires)
|
||||||
const type = c.type || (c.is_ca ? 'CA' : 'Cert')
|
const type = c.type || (c.is_ca ? 'CA' : 'Cert')
|
||||||
return (
|
return (
|
||||||
<div key={i} className={`rounded-lg border p-3 ${r ? r.bg : 'bg-gray-800/50 border-gray-700/50'}`}>
|
<div key={i} className={`rounded border px-3 py-2 ${r ? r.bg : 'bg-gray-800/50 border-gray-700/50'}`}>
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1 flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<span className="text-white text-xs font-medium truncate">{c.name || c.subject || '--'}</span>
|
||||||
<span className="text-white text-sm font-medium truncate">{c.name || c.subject || '—'}</span>
|
<span className="text-[9px] px-1 py-0.5 rounded bg-gray-700/50 text-gray-500 shrink-0">{type}</span>
|
||||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-gray-700/50 text-gray-400 shrink-0">{type}</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-500 mt-1 truncate">
|
|
||||||
{c.issuer || 'Unbekannter Aussteller'}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right shrink-0">
|
<div className="text-right shrink-0">
|
||||||
{r ? (
|
{r ? (
|
||||||
<>
|
<span className={`text-xs font-medium ${r.color}`}>{r.text}</span>
|
||||||
<div className={`text-sm font-semibold ${r.color}`}>{r.text}</div>
|
|
||||||
<div className="text-[10px] text-gray-500">
|
|
||||||
{expires ? `bis ${expires.toLocaleDateString('de-DE')}` : ''}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs text-gray-500">Kein Ablauf</div>
|
<span className="text-xs text-gray-600">--</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{r && r.pct !== undefined && (
|
{r && r.pct !== undefined && (
|
||||||
<div className="mt-2 h-1 bg-gray-700/50 rounded-full overflow-hidden">
|
<div className="mt-1 h-0.5 bg-gray-700/50 rounded-full overflow-hidden">
|
||||||
<div className={`h-full rounded-full ${r.barColor}`} style={{ width: `${r.pct}%` }} />
|
<div className={`h-full rounded-full ${r.barColor}`} style={{ width: `${r.pct}%` }} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useEffect, useState, useMemo } from 'react'
|
|||||||
import api from '../api/client'
|
import api from '../api/client'
|
||||||
import StatusBadge from '../components/StatusBadge'
|
import StatusBadge from '../components/StatusBadge'
|
||||||
import AgentPanel from '../components/AgentPanel'
|
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() {
|
export default function Agents() {
|
||||||
const [agents, setAgents] = useState([])
|
const [agents, setAgents] = useState([])
|
||||||
@ -12,6 +12,8 @@ export default function Agents() {
|
|||||||
const [sortKey, setSortKey] = useState('customer')
|
const [sortKey, setSortKey] = useState('customer')
|
||||||
const [sortDir, setSortDir] = useState('asc')
|
const [sortDir, setSortDir] = useState('asc')
|
||||||
const [selectedId, setSelectedId] = useState(null)
|
const [selectedId, setSelectedId] = useState(null)
|
||||||
|
const [showAddDialog, setShowAddDialog] = useState(false)
|
||||||
|
const [addResult, setAddResult] = useState(null)
|
||||||
|
|
||||||
const reload = () => {
|
const reload = () => {
|
||||||
Promise.all([api.getAgents(), api.getCustomers()])
|
Promise.all([api.getAgents(), api.getCustomers()])
|
||||||
@ -137,6 +139,12 @@ export default function Agents() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-xl font-bold text-white">Firewalls ({agents.length})</h1>
|
<h1 className="text-xl font-bold text-white">Firewalls ({agents.length})</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => { setShowAddDialog(true); setAddResult(null) }}
|
||||||
|
className="flex items-center gap-2 px-3 py-1.5 bg-orange-600 hover:bg-orange-500 text-white text-sm rounded transition-colors"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" /> Firewall hinzufuegen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@ -234,6 +242,161 @@ export default function Agents() {
|
|||||||
onReload={reload}
|
onReload={reload}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Firewall hinzufuegen Dialog */}
|
||||||
|
{showAddDialog && (
|
||||||
|
<AddFirewallDialog
|
||||||
|
customers={customers}
|
||||||
|
result={addResult}
|
||||||
|
onAdd={async (name, custId) => {
|
||||||
|
const res = await api.preRegisterAgent(name, custId)
|
||||||
|
setAddResult(res)
|
||||||
|
reload()
|
||||||
|
}}
|
||||||
|
onClose={() => { setShowAddDialog(false); setAddResult(null) }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }) => (
|
||||||
|
<button onClick={() => copyText(text, id)} className="ml-2 text-gray-500 hover:text-orange-400 transition-colors">
|
||||||
|
{copied === id ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
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@<FIREWALL-IP>:/tmp/
|
||||||
|
scp opnsense-plugin/install.sh root@<FIREWALL-IP>:/tmp/
|
||||||
|
|
||||||
|
# 3. Installation starten:
|
||||||
|
ssh root@<FIREWALL-IP> '/bin/sh /tmp/install.sh'
|
||||||
|
|
||||||
|
# 4. Im OPNsense WebGUI unter Services > RMM Agent:
|
||||||
|
# Backend URL: ${BACKEND_URL}
|
||||||
|
# API Key: ${API_KEY}
|
||||||
|
# Agent Name: ${name || '<NAME>'}
|
||||||
|
# -> Save & Apply`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}>
|
||||||
|
<div className="bg-gray-900 border border-gray-700 rounded-lg shadow-xl w-full max-w-2xl mx-4" onClick={e => e.stopPropagation()}>
|
||||||
|
<div className="flex items-center justify-between px-5 py-3 border-b border-gray-800">
|
||||||
|
<h2 className="text-lg font-semibold text-white">Firewall hinzufuegen</h2>
|
||||||
|
<button onClick={onClose} className="text-gray-500 hover:text-white"><X className="w-5 h-5" /></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-5 py-4 space-y-4">
|
||||||
|
{!result ? (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-gray-400 mb-1">Name *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={e => 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()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-gray-400 mb-1">Kunde (optional)</label>
|
||||||
|
<select
|
||||||
|
value={custId}
|
||||||
|
onChange={e => setCustId(e.target.value)}
|
||||||
|
className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-orange-500"
|
||||||
|
>
|
||||||
|
<option value="">Kein Kunde</option>
|
||||||
|
{customers.map(c => (
|
||||||
|
<option key={c.id} value={c.id}>{c.number} — {c.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{error && <p className="text-red-400 text-sm">{error}</p>}
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={submitting}
|
||||||
|
className="w-full py-2 bg-orange-600 hover:bg-orange-500 disabled:opacity-50 text-white text-sm font-medium rounded transition-colors"
|
||||||
|
>
|
||||||
|
{submitting ? 'Wird angelegt...' : 'Firewall anlegen'}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="bg-green-900/30 border border-green-800 rounded p-3 text-sm text-green-300">
|
||||||
|
Firewall "{result.name}" erfolgreich angelegt (ID: {result.id?.substring(0, 12)}...)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-white mb-2">Installationsanleitung</h3>
|
||||||
|
<div className="bg-gray-950 rounded p-3 text-xs font-mono text-gray-300 whitespace-pre-wrap relative">
|
||||||
|
{installCmd}
|
||||||
|
<button
|
||||||
|
onClick={() => copyText(installCmd, 'install')}
|
||||||
|
className="absolute top-2 right-2 text-gray-600 hover:text-orange-400 transition-colors"
|
||||||
|
>
|
||||||
|
{copied === 'install' ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500">Backend URL:</span>
|
||||||
|
<span className="text-white ml-2">{BACKEND_URL}</span>
|
||||||
|
<CopyBtn text={BACKEND_URL} id="url" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500">API Key:</span>
|
||||||
|
<span className="text-white ml-2 font-mono text-xs">{API_KEY}</span>
|
||||||
|
<CopyBtn text={API_KEY} id="key" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Der Agent verbindet sich nach der Installation automatisch mit dem Backend.
|
||||||
|
Hostname und IP werden beim ersten Heartbeat aktualisiert.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -273,9 +273,9 @@ class SettingsController extends ApiControllerBase
|
|||||||
public function getAction()
|
public function getAction()
|
||||||
{
|
{
|
||||||
$mdl = new RMMAgent();
|
$mdl = new RMMAgent();
|
||||||
$result = array('general' => array());
|
$result = array('rmmagent' => array('general' => array()));
|
||||||
foreach ($mdl->general->iterateItems() as $key => $node) {
|
foreach ($mdl->general->iterateItems() as $key => $node) {
|
||||||
$result['general'][$key] = (string)$node;
|
$result['rmmagent']['general'][$key] = (string)$node;
|
||||||
}
|
}
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
@ -285,20 +285,45 @@ class SettingsController extends ApiControllerBase
|
|||||||
$result = array('result' => 'failed');
|
$result = array('result' => 'failed');
|
||||||
if ($this->request->isPost()) {
|
if ($this->request->isPost()) {
|
||||||
$mdl = new RMMAgent();
|
$mdl = new RMMAgent();
|
||||||
$post = $this->request->getPost('general');
|
$post = null;
|
||||||
if (!empty($post)) {
|
|
||||||
foreach ($post as $key => $value) {
|
// OPNsense sends data as rmmagent[general][KEY]
|
||||||
$node = $mdl->general->$key;
|
$rmmagent = $this->request->getPost('rmmagent');
|
||||||
if ($node !== null) {
|
if (!empty($rmmagent) && isset($rmmagent['general'])) {
|
||||||
$node->setValue($value);
|
$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();
|
$valMsgs = $mdl->performValidation();
|
||||||
if ($valMsgs->count() > 0) {
|
if ($valMsgs->count() > 0) {
|
||||||
$result['validations'] = array();
|
$result['validations'] = array();
|
||||||
foreach ($valMsgs as $msg) {
|
foreach ($valMsgs as $msg) {
|
||||||
$result['validations']['general.' . $msg->getField()] = $msg->getMessage();
|
$result['validations']['rmmagent.general.' . $msg->getField()] = $msg->getMessage();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$mdl->serializeToConfig();
|
$mdl->serializeToConfig();
|
||||||
@ -575,6 +600,31 @@ function rmmagent_services()
|
|||||||
}
|
}
|
||||||
INCEOF
|
INCEOF
|
||||||
|
|
||||||
|
# Initialen config.xml Eintrag erstellen (falls noch nicht vorhanden)
|
||||||
|
echo " Initialen config.xml Eintrag pruefen..."
|
||||||
|
cat > /tmp/rmm_init_config.php << 'INITEOF'
|
||||||
|
<?php
|
||||||
|
require_once("/usr/local/etc/inc/config.inc");
|
||||||
|
$config = OPNsense\Core\Config::getInstance();
|
||||||
|
$xml = $config->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
|
# Cache loeschen und configd neustarten
|
||||||
rm -f /var/lib/php/cache/_usr_local_opnsense_mvc_app_views_opnsense_rmmagent_*.php
|
rm -f /var/lib/php/cache/_usr_local_opnsense_mvc_app_views_opnsense_rmmagent_*.php
|
||||||
service configd restart
|
service configd restart
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace OPNsense\RMMAgent\Api;
|
namespace OPNsense\RMMAgent\Api;
|
||||||
|
|
||||||
use OPNsense\Base\ApiControllerBase;
|
use OPNsense\Base\ApiControllerBase;
|
||||||
use OPNsense\RMMAgent\RMMAgent;
|
use OPNsense\RMMAgent\RMMAgent;
|
||||||
use OPNsense\Core\Config;
|
use OPNsense\Core\Config;
|
||||||
@ -11,28 +9,48 @@ class SettingsController extends ApiControllerBase
|
|||||||
public function getAction()
|
public function getAction()
|
||||||
{
|
{
|
||||||
$mdl = new RMMAgent();
|
$mdl = new RMMAgent();
|
||||||
$result = array('general' => array());
|
$result = array('rmmagent' => array('general' => array()));
|
||||||
|
|
||||||
foreach ($mdl->general->iterateItems() as $key => $node) {
|
foreach ($mdl->general->iterateItems() as $key => $node) {
|
||||||
$result['general'][$key] = (string)$node;
|
$result['rmmagent']['general'][$key] = (string)$node;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setAction()
|
public function setAction()
|
||||||
{
|
{
|
||||||
$result = array('result' => 'failed');
|
$result = array('result' => 'failed');
|
||||||
|
|
||||||
if ($this->request->isPost()) {
|
if ($this->request->isPost()) {
|
||||||
$mdl = new RMMAgent();
|
$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)) {
|
if (!empty($post)) {
|
||||||
foreach ($post as $key => $value) {
|
foreach ($post as $key => $value) {
|
||||||
$node = $mdl->general->$key;
|
try {
|
||||||
if ($node !== null) {
|
$node = $mdl->general->$key;
|
||||||
$node->setValue($value);
|
if ($node !== null) {
|
||||||
|
$node->setValue($value);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// skip
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -41,7 +59,7 @@ class SettingsController extends ApiControllerBase
|
|||||||
if ($valMsgs->count() > 0) {
|
if ($valMsgs->count() > 0) {
|
||||||
$result['validations'] = array();
|
$result['validations'] = array();
|
||||||
foreach ($valMsgs as $msg) {
|
foreach ($valMsgs as $msg) {
|
||||||
$result['validations']['general.' . $msg->getField()] = $msg->getMessage();
|
$result['validations']['rmmagent.general.' . $msg->getField()] = $msg->getMessage();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$mdl->serializeToConfig();
|
$mdl->serializeToConfig();
|
||||||
@ -49,7 +67,6 @@ class SettingsController extends ApiControllerBase
|
|||||||
$result['result'] = 'saved';
|
$result['result'] = 'saved';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user