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