267 lines
10 KiB
Go
267 lines
10 KiB
Go
//go:build linux
|
|
|
|
package netconfig
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
dbus "github.com/coreos/go-systemd/dbus"
|
|
)
|
|
|
|
// rcRunlevels are the SysV runlevels a network service can be started in.
|
|
// Runlevels 0, 1, and 6 are halt, single-user, and reboot, so nothing is
|
|
// registered to start there.
|
|
var rcRunlevels = []string{"2", "3", "4", "5"}
|
|
|
|
// systemdActive reports whether systemd is the running init (/run/systemd/system
|
|
// exists), the same marker systemctl uses to decide it can reach the manager.
|
|
// It is checked before dialing D-Bus so hosts running Upstart (Ubuntu 14.04) or
|
|
// SysVinit (CentOS 5/6) skip a connection that cannot usefully succeed.
|
|
func systemdActive() bool {
|
|
_, err := os.Stat("/run/systemd/system")
|
|
return err == nil
|
|
}
|
|
|
|
// commandExists reports whether name resolves on PATH.
|
|
func commandExists(name string) bool {
|
|
_, err := exec.LookPath(name)
|
|
return err == nil
|
|
}
|
|
|
|
// chkconfigOn reports whether `chkconfig --list name` shows any runlevel on,
|
|
// the RHEL-family enablement signal. Returns false when chkconfig is absent.
|
|
func chkconfigOn(ctx context.Context, name string) bool {
|
|
if !commandExists("chkconfig") {
|
|
return false
|
|
}
|
|
results, err := runCommand(ctx, "chkconfig", "--list", name)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, line := range results {
|
|
fields := strings.Fields(line)
|
|
if len(fields) == 0 || fields[0] != name {
|
|
continue
|
|
}
|
|
for _, f := range fields[1:] {
|
|
if _, status, found := strings.Cut(f, ":"); found && status == "on" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// rcRunlevelDirs returns the rcN.d directories under root that a start symlink
|
|
// can live in, covering both the Debian/Ubuntu layout (/etc/rcN.d) and the
|
|
// Slackware one (/etc/rc.d/rcN.d).
|
|
func rcRunlevelDirs(root string) []string {
|
|
dirs := make([]string, 0, len(rcRunlevels)*2)
|
|
for _, rl := range rcRunlevels {
|
|
dirs = append(dirs,
|
|
filepath.Join(root, "etc", "rc"+rl+".d"),
|
|
filepath.Join(root, "etc", "rc.d", "rc"+rl+".d"),
|
|
)
|
|
}
|
|
return dirs
|
|
}
|
|
|
|
// rcSymlinksOn reports whether an S*name start symlink exists in any rcN.d tree,
|
|
// the enablement signal for both update-rc.d (Debian/Ubuntu) and Slackware.
|
|
func rcSymlinksOn(name string) bool {
|
|
return rcSymlinksOnIn("/", name)
|
|
}
|
|
|
|
// rcSymlinksOnIn is rcSymlinksOn rooted at root, so tests can build a runlevel
|
|
// tree in a temporary directory.
|
|
func rcSymlinksOnIn(root, name string) bool {
|
|
for _, dir := range rcRunlevelDirs(root) {
|
|
if matches, _ := filepath.Glob(filepath.Join(dir, "S*"+name)); len(matches) > 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// openrcOn reports whether name is linked into an OpenRC runlevel directory
|
|
// (default or boot), the enablement signal created by `rc-update add` on Gentoo.
|
|
func openrcOn(name string) bool {
|
|
return openrcOnIn("/", name)
|
|
}
|
|
|
|
// openrcOnIn is openrcOn rooted at root, so tests can build a runlevel tree in a
|
|
// temporary directory.
|
|
func openrcOnIn(root, name string) bool {
|
|
for _, rl := range []string{"default", "boot"} {
|
|
if _, err := os.Lstat(filepath.Join(root, "etc", "runlevels", rl, name)); err == nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// sysvServiceEnabled reports whether name is registered to start at boot under
|
|
// a SysV-family init system: chkconfig (RHEL), update-rc.d rcN.d start symlinks
|
|
// (Debian/Ubuntu/Slackware), or an OpenRC runlevel (Gentoo). name is the service
|
|
// base name without a ".service" suffix. Any one mechanism reporting it on
|
|
// counts as enabled.
|
|
func sysvServiceEnabled(ctx context.Context, name string) bool {
|
|
return chkconfigOn(ctx, name) || rcSymlinksOn(name) || openrcOn(name)
|
|
}
|
|
|
|
// initSystemDiscoverable reports whether this host exposes any init system that
|
|
// sysvServiceEnabled or systemd can be asked about. It is false on minimal
|
|
// images and containers that have neither systemd nor chkconfig nor runlevel
|
|
// directories, where a backend's configuration file is the only evidence
|
|
// available and detection has nothing to corroborate it with.
|
|
func initSystemDiscoverable() bool {
|
|
if systemdActive() || commandExists("chkconfig") {
|
|
return true
|
|
}
|
|
for _, dir := range rcRunlevelDirs("/") {
|
|
if _, err := os.Stat(dir); err == nil {
|
|
return true
|
|
}
|
|
}
|
|
_, err := os.Stat("/etc/runlevels")
|
|
return err == nil
|
|
}
|
|
|
|
// networkServiceUnits are the services backend detection asks systemd about.
|
|
// They are collected here so the unit-file lookup can be filtered server side
|
|
// to just these names, which is orders of magnitude cheaper than listing every
|
|
// unit file on the host. Only these names may be passed to initState.enabled
|
|
// and initState.detected; any other name silently skips the systemd answer and
|
|
// falls through to the SysV mechanisms.
|
|
var networkServiceUnits = []string{"systemd-networkd", "NetworkManager", "network", "networking"}
|
|
|
|
// initState is a snapshot of what the host's init system reports about its
|
|
// units, taken once when the configurator is constructed. Each systemd query
|
|
// costs a D-Bus round trip, so the running units and the installed unit files
|
|
// are both fetched up front and every per-service question is answered from
|
|
// memory afterwards.
|
|
type initState struct {
|
|
// active holds the unit names ("NetworkManager.service") systemd reports as
|
|
// currently running. Empty when systemd could not be queried.
|
|
active map[string]bool
|
|
// unitFiles maps an installed unit file's base name to its UnitFileState
|
|
// ("enabled", "disabled", "generated", ...). A unit that is enabled but was
|
|
// never loaded this boot appears here and not in active, which is why both
|
|
// are read. Empty when systemd could not be queried.
|
|
unitFiles map[string]string
|
|
}
|
|
|
|
// newInitState queries systemd for the host's running units and installed unit
|
|
// files. Every step degrades to an empty map rather than an error: a host that
|
|
// cannot answer over D-Bus is not broken, it is a host whose services must be
|
|
// discovered through the SysV mechanisms instead, which enabled falls back to.
|
|
// The queries are bound by ctx so a wedged systemd cannot stall construction.
|
|
func newInitState(ctx context.Context) *initState {
|
|
s := &initState{
|
|
active: make(map[string]bool),
|
|
unitFiles: make(map[string]string),
|
|
}
|
|
|
|
// Skip D-Bus entirely when systemd is not the running init. On Ubuntu 14.04
|
|
// Upstart is PID 1 and org.freedesktop.systemd1 is served by systemd-shim,
|
|
// which accepts the connection but has no ListUnits; on CentOS 5/6 there is
|
|
// no systemd1 on the bus at all. Both would otherwise cost a failed dial and
|
|
// a failed call before reaching the SysV checks that were always going to
|
|
// answer for them.
|
|
if !systemdActive() {
|
|
return s
|
|
}
|
|
|
|
conn, err := dbus.NewWithContext(ctx)
|
|
if err != nil {
|
|
logger.Printf("unable to connect to systemd, falling back to init-script detection: %v", err)
|
|
return s
|
|
}
|
|
defer conn.Close()
|
|
|
|
units, err := conn.ListUnitsContext(ctx)
|
|
if err != nil {
|
|
logger.Printf("unable to list systemd units, falling back to init-script detection: %v", err)
|
|
}
|
|
for _, unit := range units {
|
|
if unit.ActiveState == "active" {
|
|
s.active[unit.Name] = true
|
|
}
|
|
}
|
|
|
|
// The unit files report enablement, including for units that are enabled but
|
|
// have not been loaded this boot and so never appear in ListUnits.
|
|
//
|
|
// ListUnitFilesByPatterns filters server side and answers in milliseconds;
|
|
// ListUnitFiles walks every unit file on the host and can take seconds on a
|
|
// machine with a few hundred of them. Only systemd 230 and later export the
|
|
// filtered call, so fall back to the full listing for older systemd (CentOS
|
|
// 7 ships 219). Both are read into the same map keyed by unit file name.
|
|
patterns := make([]string, 0, len(networkServiceUnits))
|
|
for _, name := range networkServiceUnits {
|
|
patterns = append(patterns, name+".service")
|
|
}
|
|
files, err := conn.ListUnitFilesByPatternsContext(ctx, nil, patterns)
|
|
if err != nil {
|
|
files, err = conn.ListUnitFilesContext(ctx)
|
|
}
|
|
if err != nil {
|
|
logger.Printf("unable to list systemd unit files, falling back to init-script detection: %v", err)
|
|
return s
|
|
}
|
|
for _, uf := range files {
|
|
s.unitFiles[filepath.Base(uf.Path)] = uf.Type
|
|
}
|
|
return s
|
|
}
|
|
|
|
// running reports whether systemd has name's service unit active right now.
|
|
func (s *initState) running(name string) bool {
|
|
return s.active[name+".service"]
|
|
}
|
|
|
|
// enabled reports whether name is registered to start at boot. systemd's
|
|
// UnitFileState is authoritative when it owns the unit outright; a "generated"
|
|
// state is not, because systemd-sysv-generator wraps an /etc/init.d script into
|
|
// a unit that carries no enablement of its own. RHEL 7 ships network.service
|
|
// that way whether or not chkconfig has it on, so a generated unit's real
|
|
// enablement is read from its SysV registration, as is any unit systemd has
|
|
// never heard of.
|
|
func (s *initState) enabled(ctx context.Context, name string) bool {
|
|
state, known := s.unitFiles[name+".service"]
|
|
if known && state != "generated" {
|
|
return state == "enabled" || state == "enabled-runtime"
|
|
}
|
|
return sysvServiceEnabled(ctx, name)
|
|
}
|
|
|
|
// detected reports whether name's service manages this host's network: either
|
|
// it is running now, or it is registered to start at boot. Enablement counts
|
|
// independently of the running state because this package writes configuration
|
|
// that must survive a reboot. A manager that is enabled but not yet started —
|
|
// a host mid-provision, a chroot, an image build, a unit whose start failed —
|
|
// still owns the network on the next boot, and systemd's ListUnits does not
|
|
// report it at all.
|
|
func (s *initState) detected(ctx context.Context, name string) bool {
|
|
return s.running(name) || s.enabled(ctx, name)
|
|
}
|
|
|
|
// networkManagerRunning reports whether NetworkManager's daemon left a runtime
|
|
// pid file. It is the running-state signal on hosts where systemd cannot be
|
|
// queried, and is unioned with the systemd signals rather than replacing them,
|
|
// so a partial answer from systemd never hides a manager this can still see.
|
|
func networkManagerRunning() bool {
|
|
for _, pidFile := range []string{
|
|
"/var/run/NetworkManager/NetworkManager.pid",
|
|
"/run/NetworkManager/NetworkManager.pid",
|
|
} {
|
|
if _, err := os.Stat(pidFile); err == nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|