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>
259 lines
7.9 KiB
Go
259 lines
7.9 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"cloudipvalidator/internal/db"
|
|
)
|
|
|
|
// --- validators ---
|
|
|
|
func (s *Server) handleConfigListValidators(w http.ResponseWriter, r *http.Request) {
|
|
validators, err := s.DB.ListValidators(r.Context())
|
|
if err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
out := make([]validatorDTO, len(validators))
|
|
for i, v := range validators {
|
|
out[i] = validatorDTO{
|
|
ValidatorID: v.ValidatorID, Hostname: v.Hostname, OSPortID: v.OSPortID,
|
|
State: v.State, LastHeartbeatAt: v.LastHeartbeatAt,
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
func (s *Server) handleConfigCreateValidator(w http.ResponseWriter, r *http.Request) {
|
|
var req createValidatorRequest
|
|
if err := readJSON(r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid body: "+err.Error())
|
|
return
|
|
}
|
|
if req.ValidatorID == "" {
|
|
writeError(w, http.StatusBadRequest, "validator_id must not be empty")
|
|
return
|
|
}
|
|
if err := s.DB.AdminCreateValidator(r.Context(), req.ValidatorID, req.OSPortID); err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, validatorDTO{ValidatorID: req.ValidatorID, OSPortID: req.OSPortID, State: "idle"})
|
|
}
|
|
|
|
func (s *Server) handleConfigUpdateValidator(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
var req updateValidatorRequest
|
|
if err := readJSON(r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid body: "+err.Error())
|
|
return
|
|
}
|
|
if err := s.DB.AdminUpdateValidatorPort(r.Context(), id, req.OSPortID); err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, okResponse{OK: true})
|
|
}
|
|
|
|
func (s *Server) handleConfigDeleteValidator(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
if err := s.DB.DeleteValidator(r.Context(), id); err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, okResponse{OK: true})
|
|
}
|
|
|
|
// --- sites ---
|
|
|
|
func (s *Server) handleConfigListSites(w http.ResponseWriter, r *http.Request) {
|
|
sites, err := s.DB.ListSites(r.Context())
|
|
if err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
out := make([]siteDTO, len(sites))
|
|
for i, site := range sites {
|
|
out[i] = siteDTO{
|
|
Index: site.Index, SiteID: site.SiteID, Hostname: site.Hostname,
|
|
State: site.State, LastHeartbeatAt: site.LastHeartbeatAt,
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
func (s *Server) handleConfigPutSite(w http.ResponseWriter, r *http.Request) {
|
|
idx, err := strconv.Atoi(r.PathValue("index"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "index must be an integer")
|
|
return
|
|
}
|
|
var req putSiteRequest
|
|
if err := readJSON(r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid body: "+err.Error())
|
|
return
|
|
}
|
|
if err := s.DB.UpsertSite(r.Context(), idx, req.SiteID); err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
// UpsertSite always resets hostname/state/last_heartbeat_at to their
|
|
// "never connected" defaults on write (see its doc comment).
|
|
writeJSON(w, http.StatusOK, siteDTO{Index: idx, SiteID: req.SiteID, State: db.SiteUnregistered})
|
|
}
|
|
|
|
func (s *Server) handleConfigDeleteSite(w http.ResponseWriter, r *http.Request) {
|
|
idx, err := strconv.Atoi(r.PathValue("index"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "index must be an integer")
|
|
return
|
|
}
|
|
if err := s.DB.DeleteSite(r.Context(), idx); err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, okResponse{OK: true})
|
|
}
|
|
|
|
// --- target groups ---
|
|
|
|
func (s *Server) handleConfigListTargets(w http.ResponseWriter, r *http.Request) {
|
|
groups, err := s.DB.ListTargetGroups(r.Context())
|
|
if err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
out := make([]targetGroupDTO, len(groups))
|
|
for i, g := range groups {
|
|
out[i] = targetGroupDTO{Name: g.Name, Targets: g.Targets}
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
func (s *Server) handleConfigPutTargetGroup(w http.ResponseWriter, r *http.Request) {
|
|
name := r.PathValue("group")
|
|
var req putTargetGroupRequest
|
|
if err := readJSON(r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid body: "+err.Error())
|
|
return
|
|
}
|
|
if err := s.DB.UpsertTargetGroup(r.Context(), name, req.Targets); err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, targetGroupDTO{Name: name, Targets: req.Targets})
|
|
}
|
|
|
|
func (s *Server) handleConfigDeleteTargetGroup(w http.ResponseWriter, r *http.Request) {
|
|
name := r.PathValue("group")
|
|
if err := s.DB.DeleteTargetGroup(r.Context(), name); err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, okResponse{OK: true})
|
|
}
|
|
|
|
// --- check types ---
|
|
|
|
func (s *Server) handleConfigListCheckTypes(w http.ResponseWriter, r *http.Request) {
|
|
checkTypes, err := s.DB.ListCheckTypes(r.Context())
|
|
if err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
out := make([]checkTypeDTO, len(checkTypes))
|
|
for i, ct := range checkTypes {
|
|
out[i] = checkTypeDTO{Name: ct.Name, Enabled: ct.Enabled, Targets: ct.TargetGroups}
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
func (s *Server) handleConfigPutCheckType(w http.ResponseWriter, r *http.Request) {
|
|
name := r.PathValue("name")
|
|
var req putCheckTypeRequest
|
|
if err := readJSON(r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid body: "+err.Error())
|
|
return
|
|
}
|
|
if err := s.DB.UpsertCheckType(r.Context(), name, req.Enabled, req.Targets); err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, checkTypeDTO{Name: name, Enabled: req.Enabled, Targets: req.Targets})
|
|
}
|
|
|
|
func (s *Server) handleConfigDeleteCheckType(w http.ResponseWriter, r *http.Request) {
|
|
name := r.PathValue("name")
|
|
if err := s.DB.DeleteCheckType(r.Context(), name); err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, okResponse{OK: true})
|
|
}
|
|
|
|
// --- orchestrator settings ---
|
|
|
|
func (s *Server) handleConfigGetOrchestratorSettings(w http.ResponseWriter, r *http.Request) {
|
|
settings, err := s.DB.GetSettings(r.Context())
|
|
if err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, orchestratorSettingsDTO{
|
|
FIPSettleSeconds: settings.FIPSettleSeconds,
|
|
HistoryRetentionCycles: settings.HistoryRetentionCycles,
|
|
})
|
|
}
|
|
|
|
// handleConfigPutOrchestratorSettings goes through the orchestrator (not a
|
|
// direct DB call, unlike the read above) because setting fip_settle_seconds
|
|
// needs cross-field validation against the static lease_ttl_seconds/
|
|
// self_check_timeout_seconds config, which only the orchestrator has.
|
|
func (s *Server) handleConfigPutOrchestratorSettings(w http.ResponseWriter, r *http.Request) {
|
|
var req orchestratorSettingsDTO
|
|
if err := readJSON(r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid body: "+err.Error())
|
|
return
|
|
}
|
|
if err := s.Orch.SetFIPSettleSeconds(r.Context(), req.FIPSettleSeconds); err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
if err := s.DB.SetHistoryRetentionCycles(r.Context(), req.HistoryRetentionCycles); err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, orchestratorSettingsDTO{
|
|
FIPSettleSeconds: req.FIPSettleSeconds,
|
|
HistoryRetentionCycles: req.HistoryRetentionCycles,
|
|
})
|
|
}
|
|
|
|
// --- prober inbound checks ---
|
|
|
|
func (s *Server) handleConfigGetInboundChecks(w http.ResponseWriter, r *http.Request) {
|
|
settings, err := s.DB.GetInboundChecks(r.Context())
|
|
if err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, inboundChecksDTO{Ports: settings.Ports, ICMP: settings.ICMP})
|
|
}
|
|
|
|
// handleConfigPutInboundChecks writes directly to the DB (unlike the
|
|
// orchestrator-settings PUT above) because port-range/duplicate validation
|
|
// is purely local — no cross-field dependency on other orchestrator config.
|
|
func (s *Server) handleConfigPutInboundChecks(w http.ResponseWriter, r *http.Request) {
|
|
var req inboundChecksDTO
|
|
if err := readJSON(r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid body: "+err.Error())
|
|
return
|
|
}
|
|
if err := s.DB.SetInboundChecks(r.Context(), req.Ports, req.ICMP); err != nil {
|
|
writeDBError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, inboundChecksDTO{Ports: req.Ports, ICMP: req.ICMP})
|
|
}
|