Files
cloud-ip-validator/internal/dashboard/handlers_ips.go
T
ayurishchevandClaude Sonnet 5 582b44f314 Add Floating IP scanning and a durable address registry with configurable history depth
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>
2026-09-23 09:52:01 +03:00

128 lines
3.9 KiB
Go

package dashboard
import (
"fmt"
"net/http"
)
type ipsPageData struct {
PageData
Items []ipQueueItem
FIPSettleSeconds int
}
type ipDetailData struct {
PageData
Detail ipDetailResponse
}
func (s *Server) handleIPsPage(w http.ResponseWriter, r *http.Request) {
items, err := s.CA.ListIPs(r.Context())
settings, settingsErr := s.CA.GetOrchestratorSettings(r.Context())
if err == nil {
err = settingsErr
}
data := ipsPageData{Items: items, FIPSettleSeconds: settings.FIPSettleSeconds}
data.ActiveNav = "ips"
data.Banner = bannerFor(err)
s.renderPage(w, "ips_page", data)
}
func (s *Server) handleIPDetail(w http.ResponseWriter, r *http.Request) {
ip := r.PathValue("ip")
detail, err := s.CA.GetIP(r.Context(), ip)
data := ipDetailData{Detail: detail}
data.ActiveNav = "ips"
data.Banner = bannerFor(err)
s.renderPage(w, "ip_detail_page", data)
}
// renderIPsTable re-fetches the current queue and renders the ips_table
// fragment, tagging actionErr (if any) on the shared error banner. Called
// after every mutating /ips/* request so the table always reflects true
// current state regardless of whether the mutation itself succeeded.
func (s *Server) renderIPsTable(w http.ResponseWriter, r *http.Request, actionErr error) {
items, listErr := s.CA.ListIPs(r.Context())
if actionErr == nil {
actionErr = listErr
}
settings, settingsErr := s.CA.GetOrchestratorSettings(r.Context())
if actionErr == nil {
actionErr = settingsErr
}
s.renderFragment(w, "ips_table", ipsPageData{Items: items, FIPSettleSeconds: settings.FIPSettleSeconds}, actionErr)
}
func (s *Server) handleIPsSubmit(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
s.renderIPsTable(w, r, fmt.Errorf("invalid form: %w", err))
return
}
addresses := splitList(r.PostFormValue("addresses"))
if len(addresses) == 0 {
s.renderIPsTable(w, r, &apiErr{Status: http.StatusBadRequest, Message: "укажите хотя бы один адрес"})
return
}
_, err := s.CA.SubmitIPs(r.Context(), addresses)
s.renderIPsTable(w, r, err)
}
func (s *Server) handleIPRecheck(w http.ResponseWriter, r *http.Request) {
ip := r.PathValue("ip")
_, err := s.CA.SubmitIPs(r.Context(), []string{ip})
s.renderIPsTable(w, r, err)
}
func (s *Server) handleIPCancel(w http.ResponseWriter, r *http.Request) {
ip := r.PathValue("ip")
err := s.CA.CancelIP(r.Context(), ip)
s.renderIPsTable(w, r, err)
}
func (s *Server) handleIPDelete(w http.ResponseWriter, r *http.Request) {
ip := r.PathValue("ip")
err := s.CA.DeleteIP(r.Context(), ip)
s.renderIPsTable(w, r, err)
}
func (s *Server) handleIPsDeleteSelected(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
s.renderIPsTable(w, r, fmt.Errorf("invalid form: %w", err))
return
}
addresses := r.Form["addresses"]
if len(addresses) == 0 {
s.renderIPsTable(w, r, &apiErr{Status: http.StatusBadRequest, Message: "ничего не выбрано"})
return
}
_, err := s.CA.DeleteIPs(r.Context(), addresses)
s.renderIPsTable(w, r, err)
}
func (s *Server) handleIPsRecheckSelected(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
s.renderIPsTable(w, r, fmt.Errorf("invalid form: %w", err))
return
}
addresses := r.Form["addresses"]
if len(addresses) == 0 {
s.renderIPsTable(w, r, &apiErr{Status: http.StatusBadRequest, Message: "ничего не выбрано"})
return
}
_, err := s.CA.SubmitIPs(r.Context(), addresses)
s.renderIPsTable(w, r, err)
}
func (s *Server) handleIPsClear(w http.ResponseWriter, r *http.Request) {
_, err := s.CA.ClearQueue(r.Context())
s.renderIPsTable(w, r, err)
}
// handleIPsScan lists the OpenStack project's free (unassociated) floating
// IPs and submits them to the check queue in one step — see
// client.ScanFloatingIPs.
func (s *Server) handleIPsScan(w http.ResponseWriter, r *http.Request) {
_, err := s.CA.ScanFloatingIPs(r.Context())
s.renderIPsTable(w, r, err)
}