Adds POST /api/v1/admin/ips/scan (plus an optional periodic ticker) to
discover free Floating IPs in the OpenStack project and feed them straight
into the check queue. More importantly, decouples check/event history from
ip_queue's lifecycle: a new ip_registry table (migration 0007) gives every
address ever submitted a durable identity, so deleting it from the queue no
longer destroys its history — it's still reachable via the new
GET /api/v1/admin/registry[/{ip}] endpoints and the dashboard's /registry
pages, with retention depth configurable in check cycles per address
(history_retention_cycles, 0 = unlimited).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
190 lines
6.1 KiB
Go
190 lines
6.1 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"cloudipvalidator/internal/db"
|
|
)
|
|
|
|
func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, okResponse{OK: true})
|
|
}
|
|
|
|
func (s *Server) handleAdminStatus(w http.ResponseWriter, r *http.Request) {
|
|
ips, err := s.DB.ListIPs(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
validators, err := s.DB.ListValidators(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
byState := map[string]int{}
|
|
for _, ip := range ips {
|
|
byState[ip.State]++
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"total_ips": len(ips),
|
|
"ips_by_state": byState,
|
|
"total_validators": len(validators),
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleAdminIPs(w http.ResponseWriter, r *http.Request) {
|
|
ips, err := s.DB.ListIPs(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, ips)
|
|
}
|
|
|
|
func (s *Server) handleAdminIPDetail(w http.ResponseWriter, r *http.Request) {
|
|
address := r.PathValue("ip")
|
|
item, err := s.DB.GetIPByAddress(r.Context(), address)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "unknown ip: "+address)
|
|
return
|
|
}
|
|
checks, err := s.DB.ListChecksForAttempt(r.Context(), item.ID, item.AttemptNumber)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
events, err := s.DB.ListEventsForIP(r.Context(), item.ID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, struct {
|
|
IP *db.IPQueueItem `json:"ip"`
|
|
Checks []db.Check `json:"checks"`
|
|
Events []db.Event `json:"events"`
|
|
}{item, checks, events})
|
|
}
|
|
|
|
func (s *Server) handleAdminValidators(w http.ResponseWriter, r *http.Request) {
|
|
validators, err := s.DB.ListValidators(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, validators)
|
|
}
|
|
|
|
// handleAdminSubmitIPs is the single entry point for both adding new
|
|
// addresses to the queue and forcing a re-check of already-finished ones —
|
|
// see db.SubmitIPs for the exact per-address rules. It's a direct DB call
|
|
// (no OpenStack interaction is needed to merely queue work), matching the
|
|
// existing admin handlers above which also bypass the orchestrator for
|
|
// reads.
|
|
func (s *Server) handleAdminSubmitIPs(w http.ResponseWriter, r *http.Request) {
|
|
var req submitIPsRequest
|
|
if err := readJSON(r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid body: "+err.Error())
|
|
return
|
|
}
|
|
result, err := s.DB.SubmitIPs(r.Context(), req.Addresses)
|
|
if err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, submitIPsResponse{
|
|
Added: emptyIfNil(result.Added),
|
|
Requeued: emptyIfNil(result.Requeued),
|
|
Reordered: emptyIfNil(result.Reordered),
|
|
SkippedInProgress: emptyIfNil(result.SkippedInProgress),
|
|
})
|
|
}
|
|
|
|
// handleAdminScanFloatingIPs lists every floating IP in the configured
|
|
// OpenStack project, filters to the free (unassociated) pool, and submits
|
|
// that address list to the check queue — see orchestrator.ScanFloatingIPs.
|
|
// Takes no body; POST is used (rather than GET) because it mutates the
|
|
// queue, matching handleAdminSubmitIPs.
|
|
func (s *Server) handleAdminScanFloatingIPs(w http.ResponseWriter, r *http.Request) {
|
|
result, scannedFree, err := s.Orch.ScanFloatingIPs(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusBadGateway, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, scanIPsResponse{
|
|
ScannedFree: scannedFree,
|
|
Added: emptyIfNil(result.Added),
|
|
Requeued: emptyIfNil(result.Requeued),
|
|
Reordered: emptyIfNil(result.Reordered),
|
|
SkippedInProgress: emptyIfNil(result.SkippedInProgress),
|
|
})
|
|
}
|
|
|
|
// handleAdminCancelIP force-stops a check in progress (or still-queued) for
|
|
// the given address. Requires the orchestrator, since a floating IP may
|
|
// need to be disassociated in OpenStack.
|
|
func (s *Server) handleAdminCancelIP(w http.ResponseWriter, r *http.Request) {
|
|
address := r.PathValue("ip")
|
|
if err := s.Orch.ForceCancel(r.Context(), address); err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, okResponse{OK: true})
|
|
}
|
|
|
|
// handleAdminDeleteIP permanently removes one address and its full history
|
|
// — unlike handleAdminCancelIP, there's nothing left to look up afterward.
|
|
// Goes through the orchestrator (not a direct DB call) since a currently
|
|
// associated floating IP needs disassociating first.
|
|
func (s *Server) handleAdminDeleteIP(w http.ResponseWriter, r *http.Request) {
|
|
address := r.PathValue("ip")
|
|
if err := s.Orch.DeleteIP(r.Context(), address); err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, okResponse{OK: true})
|
|
}
|
|
|
|
// handleAdminDeleteIPs permanently removes a specific list of addresses in
|
|
// one call — unknown addresses are reported in not_found rather than
|
|
// failing the whole request.
|
|
func (s *Server) handleAdminDeleteIPs(w http.ResponseWriter, r *http.Request) {
|
|
var req deleteIPsRequest
|
|
if err := readJSON(r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid body: "+err.Error())
|
|
return
|
|
}
|
|
if len(req.Addresses) == 0 {
|
|
writeError(w, http.StatusBadRequest, "addresses must not be empty")
|
|
return
|
|
}
|
|
result, err := s.Orch.DeleteIPs(r.Context(), req.Addresses)
|
|
if err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, deleteIPsResponse{
|
|
Deleted: emptyIfNil(result.Deleted),
|
|
NotFound: emptyIfNil(result.NotFound),
|
|
})
|
|
}
|
|
|
|
// handleAdminClearQueue permanently removes every address currently in the
|
|
// queue, including those actively being checked.
|
|
func (s *Server) handleAdminClearQueue(w http.ResponseWriter, r *http.Request) {
|
|
result, err := s.Orch.ClearQueue(r.Context())
|
|
if err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, clearQueueResponse{Deleted: emptyIfNil(result.Deleted)})
|
|
}
|
|
|
|
// emptyIfNil turns a nil slice into an empty one so these fields always
|
|
// marshal as `[]` rather than `null`.
|
|
func emptyIfNil(s []string) []string {
|
|
if s == nil {
|
|
return []string{}
|
|
}
|
|
return s
|
|
}
|