Files
cloud-ip-validator/internal/openstack/interface.go
T
ayurishchevandClaude Sonnet 5.5 1ee5757004 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>
2026-10-01 21:00:31 +03:00

83 lines
3.6 KiB
Go

// Package openstack isolates all OpenStack/Neutron interaction behind a
// small interface, so the orchestrator's business logic can be unit tested
// without a real cloud, and so the real implementation is swappable for a
// mock via config alone.
package openstack
import "context"
type FloatingIP struct {
ID string
Address string
PortID string // empty when not associated to any port
ProjectID string
}
// FloatingIPClient is the only surface the orchestrator uses to manage
// Floating IPs. It intentionally does not expose allocation/deallocation —
// this tool only associates/disassociates pre-existing floating IPs with
// validator ports; returning an address to the customer-facing pool is an
// external business process out of scope here.
type FloatingIPClient interface {
// GetFloatingIPByAddress looks up the Neutron floating-ip resource for
// a given public address. Returns ErrNotFound if no such floating IP
// is registered in the service project.
GetFloatingIPByAddress(ctx context.Context, address string) (*FloatingIP, error)
// ListFloatingIPs returns every floating IP registered in the service
// project, both associated and free — filtering to the free pool (empty
// PortID) is the caller's job. Used to discover addresses to feed into
// the check queue without an admin having to enumerate them by hand.
ListFloatingIPs(ctx context.Context) ([]FloatingIP, error)
// ListFreeFloatingIPs reads the project's floating IPs page by page
// (pageSize entries per request; <=0 means DefaultListPageSize), calling
// onPage with every page in server order — transient per-page failures
// are retried inside, so a long listing survives a flaky Neutron. Every
// entry of the page is passed, associated or free: the caller filters on
// PortID. It returns the number of non-empty pages read. An error from
// 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
// DisassociateFloatingIP detaches the floating IP from whatever port
// it's currently attached to, if any. Disassociating an already-free
// floating IP is a no-op, not an error.
DisassociateFloatingIP(ctx context.Context, fipID string) error
}
type notFoundError struct{ address string }
func (e *notFoundError) Error() string { return "floating ip not found: " + e.address }
// ErrNotFound wraps address into an error satisfying IsNotFound.
func ErrNotFound(address string) error { return &notFoundError{address} }
// IsNotFound reports whether err indicates the address has no matching
// Neutron floating-ip resource.
func IsNotFound(err error) bool {
_, ok := err.(*notFoundError)
return ok
}
// listAll collects every page of c.ListFreeFloatingIPs into one slice; it is
// the shared implementation of ListFloatingIPs on top of the paged method.
func listAll(ctx context.Context, c FloatingIPClient) ([]FloatingIP, error) {
var all []FloatingIP
if _, err := c.ListFreeFloatingIPs(ctx, DefaultListPageSize, func(page []FloatingIP) error {
all = append(all, page...)
return nil
}); err != nil {
return nil, err
}
return all, nil
}