feat(api): add live collection contract endpoints
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -556,6 +558,142 @@ func (s *Server) handleShutdown(w http.ResponseWriter, r *http.Request) {
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Server) handleCollectStart(w http.ResponseWriter, r *http.Request) {
|
||||
var req CollectRequest
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&req); err != nil {
|
||||
jsonError(w, "Invalid JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateCollectRequest(req); err != nil {
|
||||
jsonError(w, err.Error(), http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
jobID := generateJobID()
|
||||
now := time.Now().UTC()
|
||||
progress := 0
|
||||
|
||||
s.collectMu.Lock()
|
||||
s.collectJobs[jobID] = &CollectJobStatusResponse{
|
||||
JobID: jobID,
|
||||
Status: "queued",
|
||||
Progress: &progress,
|
||||
Logs: []string{"Job queued"},
|
||||
UpdatedAt: now,
|
||||
}
|
||||
s.collectMu.Unlock()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
json.NewEncoder(w).Encode(CollectJobResponse{
|
||||
JobID: jobID,
|
||||
Status: "queued",
|
||||
Message: "Collection job accepted",
|
||||
CreatedAt: now,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleCollectStatus(w http.ResponseWriter, r *http.Request) {
|
||||
jobID := strings.TrimSpace(r.PathValue("id"))
|
||||
if !isValidCollectJobID(jobID) {
|
||||
jsonError(w, "Invalid collect job id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.collectMu.RLock()
|
||||
job, ok := s.collectJobs[jobID]
|
||||
if !ok || job == nil {
|
||||
s.collectMu.RUnlock()
|
||||
jsonError(w, "Collect job not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
resp := *job
|
||||
s.collectMu.RUnlock()
|
||||
|
||||
jsonResponse(w, resp)
|
||||
}
|
||||
|
||||
func (s *Server) handleCollectCancel(w http.ResponseWriter, r *http.Request) {
|
||||
jobID := strings.TrimSpace(r.PathValue("id"))
|
||||
if !isValidCollectJobID(jobID) {
|
||||
jsonError(w, "Invalid collect job id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.collectMu.Lock()
|
||||
job, ok := s.collectJobs[jobID]
|
||||
if !ok || job == nil {
|
||||
s.collectMu.Unlock()
|
||||
jsonError(w, "Collect job not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
progress := 0
|
||||
job.Status = "canceled"
|
||||
job.Progress = &progress
|
||||
job.Logs = append(job.Logs, "Job canceled by user")
|
||||
job.Error = ""
|
||||
job.UpdatedAt = now
|
||||
resp := *job
|
||||
s.collectMu.Unlock()
|
||||
|
||||
jsonResponse(w, resp)
|
||||
}
|
||||
|
||||
func validateCollectRequest(req CollectRequest) error {
|
||||
if strings.TrimSpace(req.Host) == "" {
|
||||
return fmt.Errorf("field 'host' is required")
|
||||
}
|
||||
switch req.Protocol {
|
||||
case "redfish", "ipmi":
|
||||
default:
|
||||
return fmt.Errorf("field 'protocol' must be one of: redfish, ipmi")
|
||||
}
|
||||
if req.Port < 1 || req.Port > 65535 {
|
||||
return fmt.Errorf("field 'port' must be in range 1..65535")
|
||||
}
|
||||
if strings.TrimSpace(req.Username) == "" {
|
||||
return fmt.Errorf("field 'username' is required")
|
||||
}
|
||||
switch req.AuthType {
|
||||
case "password":
|
||||
if strings.TrimSpace(req.Password) == "" {
|
||||
return fmt.Errorf("field 'password' is required when auth_type=password")
|
||||
}
|
||||
case "token":
|
||||
if strings.TrimSpace(req.Token) == "" {
|
||||
return fmt.Errorf("field 'token' is required when auth_type=token")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("field 'auth_type' must be one of: password, token")
|
||||
}
|
||||
switch req.TLSMode {
|
||||
case "strict", "insecure":
|
||||
default:
|
||||
return fmt.Errorf("field 'tls_mode' must be one of: strict, insecure")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var collectJobIDPattern = regexp.MustCompile(`^job_[a-zA-Z0-9_-]{8,}$`)
|
||||
|
||||
func isValidCollectJobID(id string) bool {
|
||||
return collectJobIDPattern.MatchString(id)
|
||||
}
|
||||
|
||||
func generateJobID() string {
|
||||
buf := make([]byte, 8)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return fmt.Sprintf("job_%d", time.Now().UnixNano())
|
||||
}
|
||||
return fmt.Sprintf("job_%x", buf)
|
||||
}
|
||||
|
||||
func jsonResponse(w http.ResponseWriter, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(data)
|
||||
|
||||
Reference in New Issue
Block a user