package api import ( "context" "encoding/json" "errors" "net/http" "strconv" "provisioning/internal/config" "provisioning/internal/model" "provisioning/internal/orchestrator" "provisioning/internal/pxe" "provisioning/internal/statemachine" "provisioning/internal/store" "github.com/go-chi/chi/v5" ) type HostAPI struct { Hosts *store.Hosts Ops *store.Operations Locks *store.Locks Images *store.Images Runner *orchestrator.Runner Orchestrator *orchestrator.HostOrchestrator PXE *pxe.Supervisor Config *config.Config ServerTypes *config.ServerTypeRegistry } func (a *HostAPI) List(w http.ResponseWriter, r *http.Request) { hosts, err := a.Hosts.List(r.Context()) if err != nil { writeJSONErr(w, http.StatusInternalServerError, "failed to list hosts") return } writeJSON(w, http.StatusOK, hosts) } func (a *HostAPI) Get(w http.ResponseWriter, r *http.Request) { host, ok := a.hostFromURL(w, r) if !ok { return } writeJSON(w, http.StatusOK, host) } func (a *HostAPI) Create(w http.ResponseWriter, r *http.Request) { var req struct { Hostname string `json:"hostname"` MAC string `json:"mac"` ServerType string `json:"server_type"` Notes string `json:"notes"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSONErr(w, http.StatusBadRequest, "invalid json") return } if req.Hostname == "" || req.MAC == "" || req.ServerType == "" { writeJSONErr(w, http.StatusBadRequest, "hostname, mac, and server_type are required") return } if _, ok := a.ServerTypes.Get(req.ServerType); !ok { writeJSONErr(w, http.StatusBadRequest, "unknown server_type") return } if existing, err := a.Hosts.GetByHostname(r.Context(), req.Hostname); err == nil && existing != nil { writeJSONErr(w, http.StatusConflict, "a host with this hostname already exists") return } if existing, err := a.Hosts.GetByMAC(r.Context(), req.MAC); err == nil && existing != nil { writeJSONErr(w, http.StatusConflict, "a host with this MAC address already exists") return } id, err := a.Hosts.Create(r.Context(), model.Host{ Hostname: req.Hostname, MAC: req.MAC, ServerType: req.ServerType, Notes: req.Notes, }) if err != nil { writeJSONErr(w, http.StatusConflict, "host already exists: "+err.Error()) return } a.reloadPXE() host, _ := a.Hosts.Get(r.Context(), id) writeJSON(w, http.StatusCreated, host) } func (a *HostAPI) Delete(w http.ResponseWriter, r *http.Request) { id, ok := idFromURL(w, r) if !ok { return } if err := a.Hosts.Delete(r.Context(), id); err != nil { if errors.Is(err, store.ErrNotFound) { writeJSONErr(w, http.StatusNotFound, "host not found") return } writeJSONErr(w, http.StatusInternalServerError, "failed to delete host") return } a.reloadPXE() w.WriteHeader(http.StatusNoContent) } func (a *HostAPI) Rebuild(w http.ResponseWriter, r *http.Request) { host, ok := a.hostFromURL(w, r) if !ok { return } locked, _ := a.Locks.IsLocked(r.Context(), host.ID) if locked { writeJSONErr(w, http.StatusConflict, "host is locked by another operation") return } opID, err := a.Ops.Create(r.Context(), model.Operation{ HostID: host.ID, Kind: model.OpRebuildProxmox, }) if err != nil { writeJSONErr(w, http.StatusInternalServerError, "failed to create operation") return } if err := a.Locks.Acquire(r.Context(), host.ID, opID); err != nil { writeJSONErr(w, http.StatusInternalServerError, "failed to acquire lock") return } a.Runner.LogActivity(r.Context(), host.ID, model.LogInfo, "api", "Rebuild triggered via API") if err := a.Orchestrator.PrepareRebuild(r.Context(), host.ID); err != nil { _ = a.Locks.Release(r.Context(), host.ID) writeJSONErr(w, http.StatusInternalServerError, "failed to generate SSH key: "+err.Error()) return } if _, err := a.Runner.Transition(r.Context(), host.ID, statemachine.TriggerRebuildRequested); err != nil { _ = a.Locks.Release(r.Context(), host.ID) writeJSONErr(w, http.StatusConflict, err.Error()) return } a.reloadPXE() writeJSON(w, http.StatusOK, map[string]any{"ok": true, "operation_id": opID}) } func (a *HostAPI) hostFromURL(w http.ResponseWriter, r *http.Request) (*model.Host, bool) { id, ok := idFromURL(w, r) if !ok { return nil, false } host, err := a.Hosts.Get(r.Context(), id) if err != nil { if errors.Is(err, store.ErrNotFound) { writeJSONErr(w, http.StatusNotFound, "host not found") } else { writeJSONErr(w, http.StatusInternalServerError, "failed to get host") } return nil, false } return host, true } func (a *HostAPI) reloadPXE() { if a.PXE == nil { return } hosts, _ := a.Hosts.List(context.Background()) _ = a.PXE.Reload(hosts) } func idFromURL(w http.ResponseWriter, r *http.Request) (int64, bool) { s := chi.URLParam(r, "id") id, err := strconv.ParseInt(s, 10, 64) if err != nil || id <= 0 { writeJSONErr(w, http.StatusBadRequest, "invalid id") return 0, false } return id, true } func writeJSON(w http.ResponseWriter, status int, body any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(body) } func writeJSONErr(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, map[string]any{"ok": false, "error": msg}) }