go-network-configurator/options.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

242 lines
9.4 KiB
Go

package netconfig
import (
"time"
log "github.com/sirupsen/logrus"
)
// Defaults for the tunable settings. They can be overridden with the
// corresponding With* Option when constructing a Configurator.
const (
// defaultConnectivityTimeout bounds the HTTP reachability test run after an
// address or gateway change so it cannot stall the rollback logic.
defaultConnectivityTimeout = 10 * time.Second
// defaultPingCount is the number of ICMP probes used to confirm a gateway or
// candidate address is reachable.
defaultPingCount = 5
// defaultPingTimeout bounds a single ping run.
defaultPingTimeout = 20 * time.Second
// defaultBackupRetention is how many .bak.* copies are kept per original
// configuration file. Older backups beyond this number are pruned on save.
defaultBackupRetention = 5
// defaultServiceReadyTimeout bounds how long NewConfigurator waits for a
// backend's daemon to come up and finish starting. It only elapses in full
// on a host where the service is registered to run but never arrives.
defaultServiceReadyTimeout = 60 * time.Second
)
// Logger is the minimal logging surface used across the package. It is
// satisfied by the standard library *log.Logger, logrus, and most other
// logging libraries, so callers can plug in their own logger via SetLogger.
type Logger interface {
Printf(format string, args ...interface{})
Println(args ...interface{})
}
// logger is the package-wide logger. It defaults to the logrus standard
// logger and can be replaced with SetLogger. It is package scoped because the
// configuration backends and control panels log independently of any single
// Configurator instance.
var logger Logger = log.StandardLogger()
// SetLogger replaces the package-wide logger used for non-fatal diagnostics.
// It is package scoped (not a per-Configurator option) because the configuration
// backends and control panels log independently of any single Configurator
// instance; the setting therefore applies to every Configurator in the process.
// A nil logger is ignored.
func SetLogger(l Logger) {
if l != nil {
logger = l
}
}
// configOptions holds the tunable settings applied when constructing a
// Configurator. Use the With* Option helpers to set them.
type configOptions struct {
// testAddress is the URL fetched to confirm connectivity after an
// address or gateway change.
testAddress string
// skipConnectivityCheck disables the post-change internet reachability
// test and its automatic rollback.
skipConnectivityCheck bool
// connectivityTimeout bounds the HTTP reachability test.
connectivityTimeout time.Duration
// pingCount is the number of ICMP probes per ping test.
pingCount int
// pingTimeout bounds a single ping run.
pingTimeout time.Duration
// backupRetention is the number of .bak.* copies kept per original
// configuration file; <= 0 disables pruning (keeps all backups).
backupRetention int
// allowPrimaryRemoval lets RemoveAddress remove an address that is the
// primary of its family, which is otherwise refused.
allowPrimaryRemoval bool
// skipPanels makes RemoveAddress bypass all control-panel backends
// (cPanel/Plesk/InterWorx) and only remove the address from the running
// system and the network-manager configuration files.
skipPanels bool
// allowNoBackends lets NewConfigurator succeed on a Linux host where no
// network configuration backend was detected, where changes would otherwise
// apply to the running system without persisting.
allowNoBackends bool
// serviceReadyTimeout bounds how long backend detection waits for a
// daemon-backed backend (NetworkManager) to become ready; <= 0 does not
// wait at all.
serviceReadyTimeout time.Duration
}
// defaultConfigOptions returns the options used when no Option is supplied.
func defaultConfigOptions() *configOptions {
return &configOptions{
testAddress: defaultInternetTestAddress,
connectivityTimeout: defaultConnectivityTimeout,
pingCount: defaultPingCount,
pingTimeout: defaultPingTimeout,
backupRetention: defaultBackupRetention,
serviceReadyTimeout: defaultServiceReadyTimeout,
}
}
// newConfigOptions builds a configOptions from the supplied options, starting
// from the package defaults.
func newConfigOptions(opts ...Option) *configOptions {
o := defaultConfigOptions()
for _, opt := range opts {
if opt != nil {
opt(o)
}
}
return o
}
// Option configures a Configurator constructed with NewConfigurator.
type Option func(*configOptions)
// WithTestAddress overrides the URL used for the post-change connectivity
// test. The two sentinel values "test_success" and "test_fail" force the test
// to pass or fail without performing a request, which is useful in tests.
func WithTestAddress(addr string) Option {
return func(o *configOptions) {
if addr != "" {
o.testAddress = addr
}
}
}
// WithConnectivityCheck enables or disables the post-change internet
// reachability test (and the automatic rollback that depends on it). It is
// enabled by default; pass false to skip it, for example on hosts with no
// outbound internet access.
func WithConnectivityCheck(enabled bool) Option {
return func(o *configOptions) {
o.skipConnectivityCheck = !enabled
}
}
// WithSkipConnectivityCheck is a convenience option that disables the post-change
// internet reachability test and its automatic rollback. It is equivalent to
// WithConnectivityCheck(false).
func WithSkipConnectivityCheck() Option {
return WithConnectivityCheck(false)
}
// WithConnectivityTimeout sets the timeout for the HTTP reachability test run
// after an address or gateway change. A smaller value fails faster on a
// black-holed route; a larger value tolerates slower links.
func WithConnectivityTimeout(d time.Duration) Option {
return func(o *configOptions) {
if d > 0 {
o.connectivityTimeout = d
}
}
}
// WithPingCount sets the number of ICMP probes sent when confirming that a
// candidate address or gateway is reachable. More probes are more reliable on
// lossy links but take longer.
func WithPingCount(n int) Option {
return func(o *configOptions) {
if n > 0 {
o.pingCount = n
}
}
}
// WithPingTimeout sets the overall timeout for a single ping run.
func WithPingTimeout(d time.Duration) Option {
return func(o *configOptions) {
if d > 0 {
o.pingTimeout = d
}
}
}
// WithBackupRetention sets how many .bak.* copies are kept per original
// configuration file. The oldest backups beyond this number are pruned each
// time a backend saves. Pass 0 (or a negative value) to disable pruning and
// keep every backup.
func WithBackupRetention(n int) Option {
return func(o *configOptions) {
o.backupRetention = n
}
}
// WithAllowPrimaryRemoval permits RemoveAddress to remove an address that is
// the primary of its family on its interface. By default removing the primary
// is refused so a caller cannot accidentally change the system's source
// address or leave a control panel without a main IP; the intended flow is to
// SetPrimaryAddress on another IP first. Enable this only when you are
// deliberately tearing down the current primary.
func WithAllowPrimaryRemoval(allowed bool) Option {
return func(o *configOptions) {
o.allowPrimaryRemoval = allowed
}
}
// WithSkipPanels makes the configurator skip all control-panel backends
// (cPanel, Plesk, InterWorx) during mutating operations. When enabled,
// AddAddress, SetPrimaryAddress, and RemoveAddress do not tell the panels to
// reload, set the main IP, or release an IP; only the running system and the
// network-manager configuration files are changed. This is intended for
// maintenance paths where the panel must be left untouched, or where the panel
// is expected to be reconciled separately.
func WithSkipPanels(skip bool) Option {
return func(o *configOptions) {
o.skipPanels = skip
}
}
// WithAllowNoBackends lets NewConfigurator return a configurator on a Linux
// host where backend detection found no network configuration backend. By
// default that is an error: netlink would apply an address change to the
// running system, but with nothing to write it to the change would not survive
// a reboot, and the caller would never be told. Enable this when the
// configurator is only used to read state (GetInterfaces), or when the caller
// accepts runtime-only changes.
func WithAllowNoBackends(allowed bool) Option {
return func(o *configOptions) {
o.allowNoBackends = allowed
}
}
// WithServiceReadyTimeout bounds how long NewConfigurator waits for a backend
// whose configuration is made through a daemon rather than a file. Today that
// is NetworkManager: it is configured over D-Bus, so a configurator built
// before the daemon owns its bus name would find no connections to change.
//
// This matters at boot. A host that starts this program from a unit ordered
// alongside NetworkManager, rather than after it, reaches backend detection
// while the daemon is still starting; without the wait it would either fail to
// register the backend at all, or register one that reports an empty interface
// list. The wait ends as soon as the daemon answers and reports it has finished
// starting up, so a host where it is already running pays nothing.
//
// A timeout of zero or less does not wait: detection probes once and proceeds,
// which is the right choice for a caller that has already ordered itself after
// the daemon and would rather fail fast than block.
func WithServiceReadyTimeout(timeout time.Duration) Option {
return func(o *configOptions) {
o.serviceReadyTimeout = timeout
}
}