From c1d1bd8a5e01b9a2827e5f4480b23f0809d9eab0 Mon Sep 17 00:00:00 2001 From: cynfo3000 Date: Fri, 6 Mar 2026 23:02:54 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20PBS=20Collector=20=E2=80=94=20automatis?= =?UTF-8?q?che=20Erkennung,=20Datastores=20+=20Tasks=20+=20GC,=20Frontend-?= =?UTF-8?q?Tab=20'Backup=20Server'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agent-linux/collector/pbs.go | 158 +++++++++++++++++++++++ agent-linux/main.go | 5 + backend/models/system.go | 1 + frontend/src/components/ProxmoxPanel.jsx | 118 ++++++++++++++++- 4 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 agent-linux/collector/pbs.go diff --git a/agent-linux/collector/pbs.go b/agent-linux/collector/pbs.go new file mode 100644 index 0000000..e21340f --- /dev/null +++ b/agent-linux/collector/pbs.go @@ -0,0 +1,158 @@ +package collector + +import ( + "encoding/json" + "log" + "os" + "os/exec" +) + +type PBSInfo struct { + Available bool `json:"available"` + Version string `json:"version,omitempty"` + Datastores []PBSDatastore `json:"datastores,omitempty"` + Tasks []PBSTask `json:"tasks,omitempty"` + GC []PBSGCStatus `json:"gc,omitempty"` +} + +type PBSDatastore struct { + Name string `json:"name"` + Path string `json:"path,omitempty"` + Total int64 `json:"total"` + Used int64 `json:"used"` + Available int64 `json:"available"` + Namespaces int `json:"namespaces,omitempty"` +} + +type PBSTask struct { + ID string `json:"id,omitempty"` + Node string `json:"node,omitempty"` + Type string `json:"type,omitempty"` + Status string `json:"status,omitempty"` + StartTime int64 `json:"start_time,omitempty"` + EndTime int64 `json:"end_time,omitempty"` + Duration int64 `json:"duration,omitempty"` + User string `json:"user,omitempty"` + Datastore string `json:"datastore,omitempty"` +} + +type PBSGCStatus struct { + Datastore string `json:"datastore"` + Status string `json:"status,omitempty"` + LastRun int64 `json:"last_run,omitempty"` + Duration int64 `json:"duration,omitempty"` +} + +func CollectPBS() *PBSInfo { + // PBS erkennen + if _, err := os.Stat("/usr/sbin/proxmox-backup-manager"); err != nil { + return nil // PBS nicht installiert + } + + log.Printf("PBS: proxmox-backup-manager gefunden, sammle Daten...") + pbs := &PBSInfo{Available: true} + + // Version + if out, err := exec.Command("/usr/sbin/proxmox-backup-manager", "versions", "--output-format", "json").Output(); err == nil { + var versions []map[string]interface{} + if json.Unmarshal(out, &versions) == nil { + for _, v := range versions { + if pkg, ok := v["Package"].(string); ok && pkg == "proxmox-backup-server" { + if ver, ok := v["Version"].(string); ok { + pbs.Version = ver + } + } + } + } + } + + // Datastores + if out, err := exec.Command("/usr/sbin/proxmox-backup-manager", "datastore", "list", "--output-format", "json").Output(); err == nil { + var list []map[string]interface{} + if json.Unmarshal(out, &list) == nil { + for _, d := range list { + ds := PBSDatastore{} + if name, ok := d["name"].(string); ok { + ds.Name = name + } + if path, ok := d["path"].(string); ok { + ds.Path = path + } + if total, ok := d["total"].(float64); ok { + ds.Total = int64(total) + } + if used, ok := d["used"].(float64); ok { + ds.Used = int64(used) + } + if avail, ok := d["avail"].(float64); ok { + ds.Available = int64(avail) + } + pbs.Datastores = append(pbs.Datastores, ds) + } + } + } else { + log.Printf("PBS: datastore list fehler: %v", err) + } + + // Letzte Tasks (max 20) + if out, err := exec.Command("/usr/sbin/proxmox-backup-manager", "task", "list", "--limit", "20", "--output-format", "json").Output(); err == nil { + var list []map[string]interface{} + if json.Unmarshal(out, &list) == nil { + for _, t := range list { + task := PBSTask{} + if id, ok := t["upid"].(string); ok { + task.ID = id + } + if node, ok := t["node"].(string); ok { + task.Node = node + } + if typ, ok := t["type"].(string); ok { + task.Type = typ + } + if status, ok := t["status"].(string); ok { + task.Status = status + } + if start, ok := t["starttime"].(float64); ok { + task.StartTime = int64(start) + } + if end, ok := t["endtime"].(float64); ok { + task.EndTime = int64(end) + if task.StartTime > 0 { + task.Duration = task.EndTime - task.StartTime + } + } + if user, ok := t["user"].(string); ok { + task.User = user + } + pbs.Tasks = append(pbs.Tasks, task) + } + } + } else { + log.Printf("PBS: task list fehler: %v", err) + } + + // Garbage Collection Status pro Datastore + for _, ds := range pbs.Datastores { + gc := PBSGCStatus{Datastore: ds.Name} + if out, err := exec.Command("/usr/sbin/proxmox-backup-manager", "garbage-collection", "status", ds.Name, "--output-format", "json").Output(); err == nil { + var status map[string]interface{} + if json.Unmarshal(out, &status) == nil { + if upid, ok := status["upid"].(string); ok && upid != "" { + gc.Status = "running" + } else if lastRun, ok := status["last-run-starttime"].(float64); ok { + gc.LastRun = int64(lastRun) + if duration, ok := status["last-run-duration"].(float64); ok { + gc.Duration = int64(duration) + } + gc.Status = "ok" + } else { + gc.Status = "never" + } + } + } + pbs.GC = append(pbs.GC, gc) + } + + log.Printf("PBS: %d Datastores, %d Tasks gesammelt", len(pbs.Datastores), len(pbs.Tasks)) + return pbs +} diff --git a/agent-linux/main.go b/agent-linux/main.go index db33515..c46e0e5 100644 --- a/agent-linux/main.go +++ b/agent-linux/main.go @@ -171,6 +171,11 @@ func sendHeartbeat(c *client.Client, agentID string) error { systemData["proxmox"] = proxmox } + // PBS-Daten hinzufügen falls PBS installiert ist + if pbs := collector.CollectPBS(); pbs != nil { + systemData["pbs"] = pbs + } + // Backup-Daten (PVE Jobs + Task-Historie) if backups := collector.CollectBackups(); backups != nil { systemData["backups"] = backups diff --git a/backend/models/system.go b/backend/models/system.go index 64c5a45..bc8412a 100644 --- a/backend/models/system.go +++ b/backend/models/system.go @@ -23,6 +23,7 @@ type SystemData struct { CronJobs []CronJob `json:"cron_jobs,omitempty"` License *LicenseInfo `json:"license,omitempty"` Proxmox map[string]interface{} `json:"proxmox,omitempty"` + PBS map[string]interface{} `json:"pbs,omitempty"` Backups map[string]interface{} `json:"backups,omitempty"` Security *SecurityInfo `json:"security,omitempty"` PFStates *PFStatesInfo `json:"pf_states,omitempty"` diff --git a/frontend/src/components/ProxmoxPanel.jsx b/frontend/src/components/ProxmoxPanel.jsx index 600fc94..a7aec11 100644 --- a/frontend/src/components/ProxmoxPanel.jsx +++ b/frontend/src/components/ProxmoxPanel.jsx @@ -8,12 +8,13 @@ import { MonitorSmartphone, Terminal, Cable, ExternalLink, } from 'lucide-react' -const tabs = [ +const buildTabs = (hasPBS) => [ { id: 'overview', label: 'Uebersicht' }, { id: 'vms', label: 'Virtuelle Maschinen' }, { id: 'containers', label: 'Container' }, { id: 'storage', label: 'Storage' }, { id: 'zfs', label: 'ZFS' }, + ...(hasPBS ? [{ id: 'pbs', label: 'Backup Server' }] : []), { id: 'tunnel', label: 'Tunnel' }, { id: 'services', label: 'Dienste' }, { id: 'updates', label: 'Updates' }, @@ -45,7 +46,9 @@ export default function ProxmoxPanel({ agentId, customers, onClose, onReload }) const { agent, system_data: sys, status } = data const proxmox = sys?.proxmox + const pbs = sys?.pbs const cust = agent.customer_id ? customers.find((c) => c.id === agent.customer_id) : null + const tabs = buildTabs(pbs?.available) if (!proxmox?.available) { return
Keine Proxmox-Daten verfuegbar
@@ -123,6 +126,8 @@ export default function ProxmoxPanel({ agentId, customers, onClose, onReload }) ) : tab === 'updates' ? ( + ) : tab === 'pbs' ? ( + ) : tab === 'backups' ? ( ) : tab === 'agent' ? ( @@ -946,4 +951,113 @@ function formatUptime(seconds) { const m = Math.floor((seconds % 3600) / 60) if (d > 0) return `${d}d ${h}h` return `${h}h ${m}m` -} \ No newline at end of file +} +function PBSTab({ pbs }) { + if (!pbs?.available) return
Keine PBS-Daten verfuegbar
+ + const formatBytes = (b) => { + if (!b) return '—' + if (b >= 1099511627776) return `${(b / 1099511627776).toFixed(1)} TB` + if (b >= 1073741824) return `${(b / 1073741824).toFixed(1)} GB` + if (b >= 1048576) return `${(b / 1048576).toFixed(1)} MB` + return `${b} B` + } + + const formatTs = (ts) => ts ? new Date(ts * 1000).toLocaleString('de-DE') : '—' + const formatDuration = (s) => { + if (!s) return '—' + if (s < 60) return `${s}s` + if (s < 3600) return `${Math.floor(s/60)}m ${s%60}s` + return `${Math.floor(s/3600)}h ${Math.floor((s%3600)/60)}m` + } + + const taskStatusColor = (s) => { + if (!s) return 'text-gray-500' + if (s === 'OK' || s.startsWith('OK')) return 'text-green-400' + if (s.toLowerCase().includes('err') || s.toLowerCase().includes('fail')) return 'text-red-400' + if (s === 'running') return 'text-blue-400' + return 'text-yellow-400' + } + + return ( +
+ {/* Header */} +
+
+
Proxmox Backup Server
+ {pbs.version &&
Version {pbs.version}
} +
+
+ + {/* Datastores */} + {pbs.datastores?.length > 0 && ( + <> +

Datastores

+
+ {pbs.datastores.map((ds) => { + const usedPct = ds.total > 0 ? (ds.used / ds.total * 100) : 0 + const barColor = usedPct > 90 ? 'bg-red-500' : usedPct > 70 ? 'bg-yellow-500' : 'bg-green-500' + const gc = pbs.gc?.find(g => g.datastore === ds.name) + return ( +
+
+
+ {ds.name} + {ds.path && {ds.path}} +
+ {usedPct.toFixed(1)}% +
+
+
+
+
+ Belegt: {formatBytes(ds.used)} / {formatBytes(ds.total)} + Frei: {formatBytes(ds.available)} +
+ {gc && ( +
+ GC: {gc.status || '—'} + {gc.last_run > 0 && Letzter Lauf: {formatTs(gc.last_run)}} + {gc.duration > 0 && Dauer: {formatDuration(gc.duration)}} +
+ )} +
+ ) + })} +
+ + )} + + {/* Tasks */} + {pbs.tasks?.length > 0 && ( + <> +

Letzte Tasks

+
+ + + + + + + + + + + + {pbs.tasks.map((t, i) => ( + + + + + + + + ))} + +
TypStartDauerBenutzerStatus
{t.type || '—'}{formatTs(t.start_time)}{formatDuration(t.duration)}{t.user || '—'}{t.status || '—'}
+
+ + )} +
+ ) +}