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>
This commit is contained in:
1 parent
78b20fa5be
commit
582b44f314
47 files changed
+1781
-107
No files matched your search
@@ -395,6 +395,43 @@ func (o *Orchestrator) ClearQueue(ctx context.Context) (db.DeleteIPsResult, erro
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ScanFloatingIPs lists every floating IP in the OpenStack project, filters
|
||||
// to the ones not currently associated to any port (the free pool awaiting
|
||||
// validation before reissue), and submits that address list to the check
|
||||
// queue via db.SubmitIPs — the same entry point the admin API's "add
|
||||
// addresses" call uses, so add/requeue/reorder semantics are identical
|
||||
// whether the address list came from an operator or from this scan. Returns
|
||||
// the SubmitIPs outcome plus how many free floating IPs were found in total
|
||||
// (which can be larger than the sum of the SubmitIPsResult slices, since
|
||||
// addresses already mid-check are silently skipped — see db.SubmitIPs).
|
||||
func (o *Orchestrator) ScanFloatingIPs(ctx context.Context) (db.SubmitIPsResult, int, error) {
|
||||
fips, err := o.OS.ListFloatingIPs(ctx)
|
||||
if err != nil {
|
||||
return db.SubmitIPsResult{}, 0, fmt.Errorf("list floating ips: %w", err)
|
||||
}
|
||||
|
||||
var free []string
|
||||
for _, f := range fips {
|
||||
if f.PortID == "" {
|
||||
free = append(free, f.Address)
|
||||
}
|
||||
}
|
||||
|
||||
if len(free) == 0 {
|
||||
o.event(ctx, "control-api", "", nil, "fip_scan", `{"scanned_free":0}`)
|
||||
return db.SubmitIPsResult{}, 0, nil
|
||||
}
|
||||
|
||||
result, err := o.DB.SubmitIPs(ctx, free)
|
||||
if err != nil {
|
||||
return result, len(free), fmt.Errorf("submit scanned ips: %w", err)
|
||||
}
|
||||
o.event(ctx, "control-api", "", nil, "fip_scan", fmt.Sprintf(
|
||||
`{"scanned_free":%d,"added":%d,"requeued":%d,"reordered":%d,"skipped_in_progress":%d}`,
|
||||
len(free), len(result.Added), len(result.Requeued), len(result.Reordered), len(result.SkippedInProgress)))
|
||||
return result, len(free), nil
|
||||
}
|
||||
|
||||
// deletedAddressesPayload builds the event payload for the batch delete
|
||||
// operations — a proper JSON array via encoding/json rather than fmt's %q
|
||||
// slice formatting (which produces space-separated quoted strings, not
|
||||
@@ -508,6 +545,14 @@ func (o *Orchestrator) aggregateAndRelease(ctx context.Context, item db.IPQueueI
|
||||
o.event(ctx, "control-api", "", &item.ID, "aggregated",
|
||||
fmt.Sprintf(`{"result":%q,"checks":%d,"passed":%d,"missing":%d}`, result, len(checks), passCount, missing))
|
||||
|
||||
if settings, err := o.DB.GetSettings(ctx); err != nil {
|
||||
o.Log.Error("get settings for history retention", "ip_id", item.ID, "err", err)
|
||||
} else if settings.HistoryRetentionCycles > 0 {
|
||||
if err := o.DB.PruneRegistryHistory(ctx, item.RegistryID, settings.HistoryRetentionCycles); err != nil {
|
||||
o.Log.Error("prune registry history", "ip_id", item.ID, "registry_id", item.RegistryID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
if item.FIPID != "" {
|
||||
if err := o.OS.DisassociateFloatingIP(ctx, item.FIPID); err != nil {
|
||||
o.Log.Error("disassociate fip", "ip_id", item.ID, "fip_id", item.FIPID, "err", err)
|
||||
|
||||
@@ -882,3 +882,60 @@ func TestAggregationWaitsForTLSAndSSHResults(t *testing.T) {
|
||||
t.Fatalf("expected partial (missing ssh/tls-443 counted against it), got %s", ip.OverallResult)
|
||||
}
|
||||
}
|
||||
|
||||
// TestScanFloatingIPsSubmitsOnlyFreeAddresses proves ScanFloatingIPs filters
|
||||
// out floating IPs already associated to a port and only submits the free
|
||||
// pool to the check queue.
|
||||
func TestScanFloatingIPsSubmitsOnlyFreeAddresses(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
o, d, mock := newTestOrchestrator(t, 180)
|
||||
mock.Seed("fip-free-1", "1.1.1.1", "svc-project")
|
||||
mock.Seed("fip-free-2", "2.2.2.2", "svc-project")
|
||||
mock.SeedWithPort("fip-occupied", "3.3.3.3", "svc-project", "some-other-port")
|
||||
|
||||
result, scanned, err := o.ScanFloatingIPs(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("scan floating ips: %v", err)
|
||||
}
|
||||
if scanned != 2 {
|
||||
t.Fatalf("expected 2 free fips scanned, got %d", scanned)
|
||||
}
|
||||
if len(result.Added) != 2 {
|
||||
t.Fatalf("expected 2 addresses added, got %+v", result)
|
||||
}
|
||||
|
||||
ips, err := d.ListIPs(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("list ips: %v", err)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, ip := range ips {
|
||||
seen[ip.IPAddress] = true
|
||||
}
|
||||
if !seen["1.1.1.1"] || !seen["2.2.2.2"] {
|
||||
t.Fatalf("expected free addresses queued, got %+v", ips)
|
||||
}
|
||||
if seen["3.3.3.3"] {
|
||||
t.Fatalf("expected occupied address not queued, got %+v", ips)
|
||||
}
|
||||
}
|
||||
|
||||
// TestScanFloatingIPsNoFreeAddressesIsNotAnError proves scanning a project
|
||||
// with no free floating IPs (or none at all) succeeds with an empty result
|
||||
// rather than hitting SubmitIPs' "addresses must not be empty" validation.
|
||||
func TestScanFloatingIPsNoFreeAddressesIsNotAnError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
o, _, mock := newTestOrchestrator(t, 180)
|
||||
mock.SeedWithPort("fip-occupied", "3.3.3.3", "svc-project", "some-other-port")
|
||||
|
||||
result, scanned, err := o.ScanFloatingIPs(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("scan floating ips: %v", err)
|
||||
}
|
||||
if scanned != 0 {
|
||||
t.Fatalf("expected 0 free fips scanned, got %d", scanned)
|
||||
}
|
||||
if len(result.Added) != 0 {
|
||||
t.Fatalf("expected nothing added, got %+v", result)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user