Free validator ports in the cloud on clear/cancel/delete

An address still being associated (assigning_fip) has no fip_id in the
database, so "clear queue" did not detach it, and the association then
finished after the row was gone, leaving the floating IP on the validator
port for good.

- After clear/cancel/delete, ask the cloud which floating IPs sit on the
  affected validator ports (new ListFloatingIPsByPort) and detach those
  that this system queued (known in ip_registry); foreign ones are left.
- SetFIPAssociated applies only to a row still in assigning_fip; if the
  address was removed meanwhile, associateFIP detaches the floating IP.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Sonnet 5.5 committed 2026-10-01 21:00:31 +03:00
1 parent c0e300f71c
commit 1ee5757004
8 files changed
+287 -5

No files matched your search

+21
View File
@@ -239,6 +239,27 @@ func (c *Client) ListFreeFloatingIPs(ctx context.Context, pageSize int, onPage f
return paginate(ctx, pageSize, c.retry, c.fetchPage, onPage)
}
// ListFloatingIPsByPort asks Neutron for the floating IPs attached to portID.
func (c *Client) ListFloatingIPsByPort(ctx context.Context, portID string) ([]FloatingIP, error) {
pages, err := floatingips.List(c.networking, floatingips.ListOpts{PortID: portID}).AllPages(ctx)
if err != nil {
return nil, fmt.Errorf("openstack: list floating ips of port %s: %w", portID, err)
}
list, err := floatingips.ExtractFloatingIPs(pages)
if err != nil {
return nil, fmt.Errorf("openstack: extract floating ips: %w", err)
}
out := make([]FloatingIP, 0, len(list))
for _, f := range list {
proj := f.TenantID
if proj == "" {
proj = f.ProjectID
}
out = append(out, FloatingIP{ID: f.ID, Address: f.FloatingIP, PortID: f.PortID, ProjectID: proj})
}
return out, nil
}
func (c *Client) ListFloatingIPs(ctx context.Context) ([]FloatingIP, error) {
return listAll(ctx, c)
}
+5
View File
@@ -39,6 +39,11 @@ type FloatingIPClient interface {
// onPage stops the walk and is returned as-is.
ListFreeFloatingIPs(ctx context.Context, pageSize int, onPage func(page []FloatingIP) error) (pages int, err error)
// ListFloatingIPsByPort returns the floating IPs currently attached to
// the given port — read from the cloud, not from our database, so it
// also finds attachments we lost track of.
ListFloatingIPsByPort(ctx context.Context, portID string) ([]FloatingIP, error)
// AssociateFloatingIP attaches the floating IP to the given Neutron
// port (the validator's primary NIC port).
AssociateFloatingIP(ctx context.Context, fipID, portID string) error
+12
View File
@@ -159,6 +159,18 @@ func (m *MockClient) ListFloatingIPs(ctx context.Context) ([]FloatingIP, error)
return listAll(ctx, m)
}
func (m *MockClient) ListFloatingIPsByPort(ctx context.Context, portID string) ([]FloatingIP, error) {
m.mu.Lock()
defer m.mu.Unlock()
var out []FloatingIP
for _, f := range m.fips {
if f.PortID == portID {
out = append(out, *f)
}
}
return out, nil
}
func (m *MockClient) AssociateFloatingIP(ctx context.Context, fipID, portID string) error {
m.mu.Lock()
defer m.mu.Unlock()