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".
78 lines
2.6 KiB
Go
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:
|
|
}
|
|
}
|
|
}
|