137 lines
3.4 KiB
Go
137 lines
3.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"forge-mesh/internal/alerts"
|
|
"forge-mesh/internal/auth"
|
|
"forge-mesh/internal/fleet"
|
|
)
|
|
|
|
// CrucibleHandler serves batch terminal endpoints.
|
|
type CrucibleHandler struct {
|
|
Store *fleet.Store
|
|
Hub *fleet.Hub
|
|
Crucible *fleet.CrucibleStore
|
|
Alerts *alerts.Notifier
|
|
OperatorClearance int
|
|
}
|
|
|
|
type batchRequest struct {
|
|
Command string `json:"command"`
|
|
HostIDs []string `json:"host_ids"`
|
|
All bool `json:"all"`
|
|
}
|
|
|
|
func (h *CrucibleHandler) Dispatch(w http.ResponseWriter, r *http.Request) {
|
|
if err := fleet.CheckAction(h.OperatorClearance, fleet.ActionCrucible); err != nil {
|
|
auth.JSON(w, http.StatusForbidden, map[string]any{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
var req batchRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
req.Command = strings.TrimSpace(req.Command)
|
|
if req.Command == "" {
|
|
http.Error(w, "command required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
hostIDs := req.HostIDs
|
|
if req.All || len(hostIDs) == 0 {
|
|
hosts, err := h.Store.ListHosts()
|
|
if err != nil {
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
for _, host := range hosts {
|
|
if host.Status == "online" || host.Status == "mining" {
|
|
hostIDs = append(hostIDs, host.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
job := h.Crucible.Create(req.Command, hostIDs)
|
|
action := mapCommandToAction(req.Command)
|
|
|
|
for _, hostID := range hostIDs {
|
|
host, _ := h.Store.GetHost(hostID)
|
|
hostname := hostID
|
|
if host != nil {
|
|
hostname = host.Hostname
|
|
}
|
|
|
|
if err := fleet.CheckAction(h.OperatorClearance, action); err != nil {
|
|
h.Crucible.AddResult(job.ID, fleet.BatchResult{
|
|
HostID: hostID, Hostname: hostname,
|
|
Status: "denied", Message: err.Error(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
cmd, err := h.Hub.DispatchCommand(hostID, action, map[string]any{"raw": req.Command})
|
|
if err != nil {
|
|
h.Crucible.AddResult(job.ID, fleet.BatchResult{
|
|
HostID: hostID, Hostname: hostname,
|
|
Status: "error", Message: err.Error(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
h.Crucible.AddResult(job.ID, fleet.BatchResult{
|
|
HostID: hostID, Hostname: hostname,
|
|
Status: "dispatched", CommandID: cmd.ID,
|
|
})
|
|
}
|
|
|
|
h.Crucible.Complete(job.ID, "completed")
|
|
|
|
if h.Alerts != nil && h.Alerts.Enabled() {
|
|
h.Alerts.Send(alerts.CrucibleEvent(job.ID, len(hostIDs), req.Command))
|
|
}
|
|
|
|
_ = h.Store.InsertSeerEvent("", "crucible", fmt.Sprintf(`{"job_id":"%s","command":%q}`, job.ID, req.Command))
|
|
|
|
auth.JSON(w, http.StatusOK, job)
|
|
}
|
|
|
|
func (h *CrucibleHandler) GetJob(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
job, ok := h.Crucible.Get(id)
|
|
if !ok {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
auth.JSON(w, http.StatusOK, job)
|
|
}
|
|
|
|
func (h *CrucibleHandler) History(w http.ResponseWriter, r *http.Request) {
|
|
auth.JSON(w, http.StatusOK, map[string]any{
|
|
"jobs": h.Crucible.History(20),
|
|
})
|
|
}
|
|
|
|
func mapCommandToAction(cmd string) string {
|
|
lower := strings.ToLower(strings.TrimSpace(cmd))
|
|
switch {
|
|
case strings.HasPrefix(lower, "pause"):
|
|
return fleet.ActionPause
|
|
case strings.HasPrefix(lower, "resume"):
|
|
return fleet.ActionResume
|
|
case strings.HasPrefix(lower, "reboot"):
|
|
return fleet.ActionReboot
|
|
case strings.HasPrefix(lower, "screenshot"):
|
|
return fleet.ActionScreenshot
|
|
case strings.HasPrefix(lower, "shell"):
|
|
return fleet.ActionShell
|
|
default:
|
|
return fleet.ActionStatus
|
|
}
|
|
}
|