go-network-configurator/services.go
James Coleman fae43247de Switch unit tests to testify
Bring an administratively down interface up in AddAddress before writing
its addresses and routes. The kernel installs an address's connected
route only while its link is up, so adding a gateway to a down interface
was rejected as unreachable. A link raised this way is part of the
pre-change state AddAddress restores, so it is returned to down on any
path that does not complete.

Also fix the rollback masking its own error: deleting the address it
added takes the default route with it, since that route resolved its
nexthop through the address's connected subnet. The following RouteDel
then answered ESRCH and was reported in place of "aborted operation due
to loss of internet".
2026-07-10 08:52:52 -05:00

78 lines
2.6 KiB
Go

package netconfig
import (
"context"
"fmt"
"time"
)
// serviceReadyPollInterval is how often waitForServiceReady re-probes. A daemon
// coming up at boot takes seconds, so probing a few times a second costs little
// and returns promptly once it arrives. It is a variable only so tests can wait
// on the loop without sleeping for real.
var serviceReadyPollInterval = 250 * time.Millisecond
// readyProbe reports whether a service is ready to be configured. An error is a
// probe that could not reach the service, which during boot is indistinguishable
// from a service that has not started yet; it is therefore not fatal on its own
// and is only surfaced if the wait runs out.
type readyProbe func() (bool, error)
// waitForServiceReady blocks until probe reports the named service ready, the
// timeout elapses, or ctx is cancelled.
//
// Some backends are configured through a running daemon rather than a file, so
// a configurator constructed before that daemon is up would hold a handle that
// reports nothing and accepts no changes. This program can be started early
// enough in boot to lose that race — from a unit ordered alongside the daemon
// rather than after it — which is what the wait is for.
//
// A timeout of zero or less does not wait: probe runs once and its result is
// reported, so a caller already ordered after the daemon fails fast instead of
// blocking. A service that is already up returns on the first probe and pays no
// delay either way.
func waitForServiceReady(ctx context.Context, name string, timeout time.Duration, probe readyProbe) error {
if timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}
ticker := time.NewTicker(serviceReadyPollInterval)
defer ticker.Stop()
waited := false
for {
ready, probeErr := probe()
if ready {
if waited {
logger.Printf("%s: became ready", name)
}
return nil
}
// Not waiting: report why the single probe said no.
if timeout <= 0 {
if probeErr != nil {
return fmt.Errorf("%s is not ready: %w", name, probeErr)
}
return fmt.Errorf("%s is not ready", name)
}
if !waited {
waited = true
logger.Printf("%s: waiting up to %s for the service to become ready", name, timeout)
}
select {
case <-ctx.Done():
// The last probe error explains the wait better than "deadline
// exceeded" does, so prefer it when there was one.
if probeErr != nil {
return fmt.Errorf("timed out waiting for %s: %w", name, probeErr)
}
return fmt.Errorf("timed out waiting for %s: %w", name, ctx.Err())
case <-ticker.C:
}
}
}