diff --git a/agent-linux/ws/handler.go b/agent-linux/ws/handler.go
index d90d79b..b82cf02 100644
--- a/agent-linux/ws/handler.go
+++ b/agent-linux/ws/handler.go
@@ -47,6 +47,8 @@ func (h *Handler) HandleCommand(msg Message) {
response = h.handleUpdate(msg)
case "zpoolscrub":
response = h.handleZpoolScrub(msg)
+ case "proxmox_action":
+ response = h.handleProxmoxAction(msg)
case "pty_start":
response = h.handlePTYStart(msg)
case "pty_stop":
@@ -481,3 +483,65 @@ func (h *Handler) handleZpoolScrub(msg Message) Message {
}
return response
}
+
+// handleProxmoxAction: VM/CT starten oder stoppen via pvesh
+func (h *Handler) handleProxmoxAction(msg Message) Message {
+ response := Message{Type: "response", ID: msg.ID}
+
+ vmType, _ := msg.Params["type"].(string) // "vm" oder "ct"
+ action, _ := msg.Params["action"].(string) // "start", "stop", "shutdown"
+ vmidFloat, ok := msg.Params["vmid"].(float64)
+ if !ok {
+ response.Status = "error"
+ response.Error = "Parameter 'vmid' erforderlich"
+ return response
+ }
+ vmid := int(vmidFloat)
+
+ if vmType != "vm" && vmType != "ct" {
+ response.Status = "error"
+ response.Error = "Parameter 'type' muss 'vm' oder 'ct' sein"
+ return response
+ }
+ if action != "start" && action != "stop" && action != "shutdown" {
+ response.Status = "error"
+ response.Error = "Parameter 'action' muss 'start', 'stop' oder 'shutdown' sein"
+ return response
+ }
+
+ // Node-Name ermitteln
+ nodeName, _ := os.Hostname()
+ if nodeName == "" {
+ nodeName = "localhost"
+ }
+
+ // pvesh-Pfad aufbauen
+ var apiPath string
+ if vmType == "vm" {
+ apiPath = fmt.Sprintf("/nodes/%s/qemu/%d/status/%s", nodeName, vmid, action)
+ } else {
+ if action == "shutdown" {
+ action = "stop" // LXC kennt kein "shutdown", stop reicht
+ }
+ apiPath = fmt.Sprintf("/nodes/%s/lxc/%d/status/%s", nodeName, vmid, action)
+ }
+
+ log.Printf("Proxmox Action: pvesh create %s", apiPath)
+
+ cmd := exec.Command("/usr/bin/pvesh", "create", apiPath, "--output-format", "json")
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ log.Printf("Proxmox Action Fehler: %v — %s", err, string(output))
+ response.Status = "error"
+ response.Error = fmt.Sprintf("pvesh fehlgeschlagen: %v — %s", err, strings.TrimSpace(string(output)))
+ return response
+ }
+
+ log.Printf("Proxmox Action erfolgreich: %s %s %d", action, vmType, vmid)
+ response.Status = "ok"
+ response.Data = map[string]interface{}{
+ "message": fmt.Sprintf("%s %d: %s ausgeführt", vmType, vmid, action),
+ "output": strings.TrimSpace(string(output)),
+ }
+ return response
+}
diff --git a/backend/api/tunnel_proxy.go b/backend/api/tunnel_proxy.go
index 9a81180..25e6239 100644
--- a/backend/api/tunnel_proxy.go
+++ b/backend/api/tunnel_proxy.go
@@ -37,6 +37,7 @@ func (h *Handler) setupTunnelRoutes(mux *http.ServeMux, hub *ws.Hub) {
mux.HandleFunc("DELETE /api/v1/agents/{id}/wireguard/peers/{uuid}", h.wgDeletePeer(hub))
// Backup-Endpoints
+ mux.HandleFunc("POST /api/v1/agents/{id}/proxmox/action", h.proxmoxAction(hub))
mux.HandleFunc("POST /api/v1/agents/{id}/backup", h.triggerBackup(hub))
mux.HandleFunc("GET /api/v1/agents/{id}/backups", h.listBackups())
mux.HandleFunc("GET /api/v1/agents/{id}/backups/{backup_id}", h.getBackup())
@@ -265,6 +266,36 @@ func (h *Handler) agentReboot(hub *ws.Hub) http.HandlerFunc {
}
}
+// proxmoxAction: VM/CT starten oder stoppen
+func (h *Handler) proxmoxAction(hub *ws.Hub) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ agentID := r.PathValue("id")
+
+ var req struct {
+ Type string `json:"type"` // "vm" oder "ct"
+ VMID int `json:"vmid"`
+ Action string `json:"action"` // "start", "stop", "shutdown"
+ }
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ writeError(w, http.StatusBadRequest, "Ungültiger Request-Body")
+ return
+ }
+ if req.VMID <= 0 || req.Type == "" || req.Action == "" {
+ writeError(w, http.StatusBadRequest, "type, vmid und action erforderlich")
+ return
+ }
+
+ params := map[string]interface{}{
+ "type": req.Type,
+ "vmid": float64(req.VMID),
+ "action": req.Action,
+ }
+
+ h.auditLog(r, "proxmox_action", "agent", agentID, "", fmt.Sprintf("%s %s %d", req.Action, req.Type, req.VMID))
+ h.sendCommandAndWait(hub, agentID, "proxmox_action", params, 60, w)
+ }
+}
+
// sendAgentCommand erstellt einen einfachen Command-Handler ohne Request-Body
func (h *Handler) sendAgentCommand(hub *ws.Hub, command string, params map[string]interface{}, timeoutSec int) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js
index 466359f..9eb94c6 100644
--- a/frontend/src/api/client.js
+++ b/frontend/src/api/client.js
@@ -224,6 +224,10 @@ class ApiClient {
return this.post(`/api/v1/agents/${agentId}/exec`, { command, timeout })
}
+ proxmoxAction(agentId, type, vmid, action) {
+ return this.post(`/api/v1/agents/${agentId}/proxmox/action`, { type, vmid, action })
+ }
+
// Users
getUsers() {
return this.get('/api/v1/users')
diff --git a/frontend/src/components/ProxmoxPanel.jsx b/frontend/src/components/ProxmoxPanel.jsx
index af69f3f..502f15c 100644
--- a/frontend/src/components/ProxmoxPanel.jsx
+++ b/frontend/src/components/ProxmoxPanel.jsx
@@ -117,9 +117,9 @@ export default function ProxmoxPanel({ agentId, customers, onClose, onReload })
) : tab === 'overview' ? (