Update-Commands: update, major_update, update_check, reboot
Agent:
- update: opnsense-update + pkg upgrade -y, optionaler Reboot
- major_update: opnsense-update -r <version>, optionaler Reboot
- update_check: Prueft Core- und Paket-Updates, liefert Details
- reboot: Shutdown mit konfigurierbarer Verzoegerung
Backend:
- Synchrone API-Endpoints (warten auf Agent-Response)
- Response-Waiter-System im Hub (Command-ID basiert)
- Konfigurierbare Timeouts pro Command-Typ
- POST /agents/{id}/update[-check] und /major-update und /reboot
README mit Update-Beispielen und API-Doku aktualisiert
This commit is contained in:
@@ -12,6 +12,10 @@ import (
|
||||
func (h *Handler) setupTunnelRoutes(mux *http.ServeMux, hub *ws.Hub) {
|
||||
// Command-Endpoints
|
||||
mux.HandleFunc("POST /api/v1/agents/{id}/exec", h.executeCommand(hub))
|
||||
mux.HandleFunc("POST /api/v1/agents/{id}/update", h.agentUpdate(hub))
|
||||
mux.HandleFunc("POST /api/v1/agents/{id}/major-update", h.agentMajorUpdate(hub))
|
||||
mux.HandleFunc("POST /api/v1/agents/{id}/update-check", h.agentUpdateCheck(hub))
|
||||
mux.HandleFunc("POST /api/v1/agents/{id}/reboot", h.agentReboot(hub))
|
||||
mux.HandleFunc("POST /api/v1/agents/{id}/tunnel", h.openTunnel(hub))
|
||||
mux.HandleFunc("DELETE /api/v1/agents/{id}/tunnel/{tunnel_id}", h.closeTunnel(hub))
|
||||
mux.HandleFunc("GET /api/v1/agents/{id}/tunnels", h.listTunnels(hub))
|
||||
@@ -178,6 +182,117 @@ func (h *Handler) httpProxy(hub *ws.Hub) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Update Commands ---
|
||||
|
||||
func (h *Handler) agentUpdateCheck(hub *ws.Hub) http.HandlerFunc {
|
||||
return h.sendAgentCommand(hub, "update_check", nil, 120)
|
||||
}
|
||||
|
||||
func (h *Handler) agentUpdate(hub *ws.Hub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
agentID := r.PathValue("id")
|
||||
|
||||
var req struct {
|
||||
Reboot bool `json:"reboot"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
params := map[string]interface{}{
|
||||
"reboot": req.Reboot,
|
||||
}
|
||||
|
||||
h.sendCommandAndWait(hub, agentID, "update", params, 900, w)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) agentMajorUpdate(hub *ws.Hub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
agentID := r.PathValue("id")
|
||||
|
||||
var req struct {
|
||||
Version string `json:"version"`
|
||||
Reboot bool `json:"reboot"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Version == "" {
|
||||
writeError(w, http.StatusBadRequest, "Parameter 'version' erforderlich (z.B. '26.7')")
|
||||
return
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
"version": req.Version,
|
||||
"reboot": req.Reboot,
|
||||
}
|
||||
|
||||
h.sendCommandAndWait(hub, agentID, "major_update", params, 1200, w)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) agentReboot(hub *ws.Hub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
agentID := r.PathValue("id")
|
||||
|
||||
var req struct {
|
||||
Delay int `json:"delay"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
params := map[string]interface{}{}
|
||||
if req.Delay > 0 {
|
||||
params["delay"] = req.Delay
|
||||
}
|
||||
|
||||
h.sendCommandAndWait(hub, agentID, "reboot", params, 30, 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) {
|
||||
agentID := r.PathValue("id")
|
||||
h.sendCommandAndWait(hub, agentID, command, params, timeoutSec, w)
|
||||
}
|
||||
}
|
||||
|
||||
// sendCommandAndWait schickt ein Command an den Agent und wartet auf Response
|
||||
func (h *Handler) sendCommandAndWait(hub *ws.Hub, agentID, command string, params map[string]interface{}, timeoutSec int, w http.ResponseWriter) {
|
||||
if !hub.IsAgentConnected(agentID) {
|
||||
writeError(w, http.StatusServiceUnavailable, "Agent nicht verbunden")
|
||||
return
|
||||
}
|
||||
|
||||
cmdID := generateID()
|
||||
|
||||
cmd := map[string]interface{}{
|
||||
"type": "command",
|
||||
"id": cmdID,
|
||||
"command": command,
|
||||
}
|
||||
if params != nil {
|
||||
cmd["params"] = params
|
||||
}
|
||||
|
||||
// Response-Channel registrieren
|
||||
responseCh := hub.RegisterResponseWaiter(cmdID)
|
||||
defer hub.UnregisterResponseWaiter(cmdID)
|
||||
|
||||
cmdJSON, _ := json.Marshal(cmd)
|
||||
if err := hub.SendToAgent(agentID, cmdJSON); err != nil {
|
||||
log.Printf("Command '%s' senden fehlgeschlagen: %v", command, err)
|
||||
writeError(w, http.StatusInternalServerError, "Command senden fehlgeschlagen")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Command '%s' (ID: %s) an Agent %s gesendet, warte auf Response...", command, cmdID, agentID)
|
||||
|
||||
// Auf Response warten
|
||||
select {
|
||||
case resp := <-responseCh:
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
case <-time.After(time.Duration(timeoutSec) * time.Second):
|
||||
writeError(w, http.StatusGatewayTimeout, "Timeout: Agent hat nicht rechtzeitig geantwortet")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) hubStatus(hub *ws.Hub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
stats := hub.GetAgentStats()
|
||||
|
||||
+57
-8
@@ -22,14 +22,19 @@ type Hub struct {
|
||||
broadcast chan []byte
|
||||
|
||||
tunnelManager *TunnelManager
|
||||
|
||||
// Response-Waiters: Command-ID -> Response-Channel
|
||||
responseWaiters map[string]chan map[string]interface{}
|
||||
waitersMux sync.RWMutex
|
||||
}
|
||||
|
||||
func NewHub() *Hub {
|
||||
hub := &Hub{
|
||||
agents: make(map[string]*AgentConnection),
|
||||
register: make(chan *AgentConnection),
|
||||
unregister: make(chan *AgentConnection),
|
||||
broadcast: make(chan []byte, 256),
|
||||
agents: make(map[string]*AgentConnection),
|
||||
register: make(chan *AgentConnection),
|
||||
unregister: make(chan *AgentConnection),
|
||||
broadcast: make(chan []byte, 256),
|
||||
responseWaiters: make(map[string]chan map[string]interface{}),
|
||||
}
|
||||
|
||||
hub.tunnelManager = NewTunnelManager(hub)
|
||||
@@ -193,6 +198,22 @@ func (h *Hub) cleanupInactiveAgents() {
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterResponseWaiter registriert einen Channel fuer eine Command-Response
|
||||
func (h *Hub) RegisterResponseWaiter(cmdID string) chan map[string]interface{} {
|
||||
ch := make(chan map[string]interface{}, 1)
|
||||
h.waitersMux.Lock()
|
||||
h.responseWaiters[cmdID] = ch
|
||||
h.waitersMux.Unlock()
|
||||
return ch
|
||||
}
|
||||
|
||||
// UnregisterResponseWaiter entfernt einen Response-Waiter
|
||||
func (h *Hub) UnregisterResponseWaiter(cmdID string) {
|
||||
h.waitersMux.Lock()
|
||||
delete(h.responseWaiters, cmdID)
|
||||
h.waitersMux.Unlock()
|
||||
}
|
||||
|
||||
// GetTunnelManager gibt den TunnelManager zurück (für API-Handler)
|
||||
func (h *Hub) GetTunnelManager() *TunnelManager {
|
||||
return h.tunnelManager
|
||||
@@ -245,10 +266,38 @@ func (h *Hub) ProcessAgentMessage(agentID string, messageType int, data []byte)
|
||||
return nil // Ignorieren wenn nicht parsebar
|
||||
}
|
||||
|
||||
// Tunnel-Connect Responses verarbeiten
|
||||
if msg.Type == "response" && msg.Status == "ok" && msg.Data != nil {
|
||||
if sessionID, ok := msg.Data["session_id"].(string); ok {
|
||||
h.tunnelManager.ConfirmSession(sessionID)
|
||||
if msg.Type == "response" {
|
||||
// Tunnel-Connect Responses verarbeiten
|
||||
if msg.Status == "ok" && msg.Data != nil {
|
||||
if sessionID, ok := msg.Data["session_id"].(string); ok {
|
||||
h.tunnelManager.ConfirmSession(sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
// Response an wartende API-Handler weiterleiten
|
||||
if msg.ID != "" {
|
||||
h.waitersMux.RLock()
|
||||
ch, exists := h.responseWaiters[msg.ID]
|
||||
h.waitersMux.RUnlock()
|
||||
|
||||
if exists {
|
||||
resp := map[string]interface{}{
|
||||
"status": msg.Status,
|
||||
"data": msg.Data,
|
||||
}
|
||||
if msg.Status == "error" {
|
||||
var errMsg struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
json.Unmarshal(data, &errMsg)
|
||||
resp["error"] = errMsg.Error
|
||||
}
|
||||
|
||||
select {
|
||||
case ch <- resp:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user