go-network-configurator/services_linux_unit_test.go

168 lines
6.3 KiB
Go

//go:build linux
package netconfig
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// absentService is a service name no host can plausibly have registered, so
// sysvServiceEnabled is guaranteed to answer false for it. It lets the systemd
// branches of initState.enabled be tested without the SysV fallback ever
// masking the result.
const absentService = "netconfig-nonexistent-service"
// A unit file state systemd owns outright is authoritative: only "enabled" and
// "enabled-runtime" count, and the SysV fallback is never consulted.
func TestInitStateEnabledTrustsSystemdUnitFileState(t *testing.T) {
ctx := context.Background()
cases := map[string]bool{
"enabled": true,
"enabled-runtime": true,
"disabled": false,
"static": false,
"masked": false,
}
for state, want := range cases {
s := &initState{
active: map[string]bool{},
unitFiles: map[string]string{absentService + ".service": state},
}
assert.Equal(t, want, s.enabled(ctx, absentService),
"unit file state %q", state)
}
}
// A "generated" unit is a systemd-sysv-generator wrapper around an init.d
// script. It carries no enablement of its own, so it must not be read as
// enabled — RHEL 7 ships network.service that way whether or not chkconfig has
// it on. Enablement has to come from the SysV registration, which for a service
// no host has is absent.
func TestInitStateEnabledDoesNotTrustGeneratedUnits(t *testing.T) {
s := &initState{
active: map[string]bool{},
unitFiles: map[string]string{absentService + ".service": "generated"},
}
assert.False(t, s.enabled(context.Background(), absentService),
"a generated unit with no SysV registration must not read as enabled")
}
// A service systemd has never heard of falls through to the SysV mechanisms
// rather than being reported as disabled.
func TestInitStateEnabledFallsBackForUnknownUnits(t *testing.T) {
s := &initState{active: map[string]bool{}, unitFiles: map[string]string{}}
assert.False(t, s.enabled(context.Background(), absentService))
}
// detected is the union of running and enabled. Enablement must count on its
// own: a manager that is enabled but has not started this boot never appears in
// systemd's ListUnits, yet it owns the network after the next reboot and its
// configuration still has to be written. Equally, a manager that is running but
// disabled owns the network right now.
func TestInitStateDetectedUnionsRunningAndEnabled(t *testing.T) {
ctx := context.Background()
unit := absentService + ".service"
enabledNotRunning := &initState{
active: map[string]bool{},
unitFiles: map[string]string{unit: "enabled"},
}
assert.True(t, enabledNotRunning.detected(ctx, absentService),
"an enabled service that is not running must be detected")
runningNotEnabled := &initState{
active: map[string]bool{unit: true},
unitFiles: map[string]string{unit: "disabled"},
}
assert.True(t, runningNotEnabled.detected(ctx, absentService),
"a running service that is not enabled must be detected")
neither := &initState{
active: map[string]bool{},
unitFiles: map[string]string{unit: "disabled"},
}
assert.False(t, neither.detected(ctx, absentService))
}
// running reads systemd's active-unit set, keyed by the full unit name.
func TestInitStateRunning(t *testing.T) {
s := &initState{
active: map[string]bool{"NetworkManager.service": true},
unitFiles: map[string]string{},
}
assert.True(t, s.running("NetworkManager"))
assert.False(t, s.running("systemd-networkd"))
}
// An S* start symlink in any rcN.d tree marks the service as enabled, in both
// the Debian/Ubuntu (/etc/rcN.d) and Slackware (/etc/rc.d/rcN.d) layouts. A K*
// kill symlink does not.
func TestRcSymlinksOnIn(t *testing.T) {
for _, dir := range []string{"etc/rc2.d", "etc/rc.d/rc3.d", "etc/rc5.d"} {
t.Run(dir, func(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, dir), 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(root, dir, "S01networking"), nil, 0o644))
assert.True(t, rcSymlinksOnIn(root, "networking"))
assert.False(t, rcSymlinksOnIn(root, "network"),
"S01networking must not match the distinct service %q", "network")
})
}
// A kill symlink means the service is stopped in that runlevel, not started.
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, "etc/rc2.d"), 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(root, "etc/rc2.d", "K01networking"), nil, 0o644))
assert.False(t, rcSymlinksOnIn(root, "networking"))
// Runlevels 0, 1, and 6 are halt, single-user, and reboot; nothing starts there.
root = t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, "etc/rc1.d"), 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(root, "etc/rc1.d", "S01networking"), nil, 0o644))
assert.False(t, rcSymlinksOnIn(root, "networking"))
assert.False(t, rcSymlinksOnIn(t.TempDir(), "networking"),
"an empty root has no enabled services")
}
// OpenRC records enablement as a symlink into a runlevel directory.
func TestOpenrcOnIn(t *testing.T) {
for _, rl := range []string{"default", "boot"} {
t.Run(rl, func(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, "etc/runlevels", rl), 0o755))
require.NoError(t, os.Symlink("/etc/init.d/net.lo",
filepath.Join(root, "etc/runlevels", rl, "net.lo")))
assert.True(t, openrcOnIn(root, "net.lo"))
assert.False(t, openrcOnIn(root, "NetworkManager"))
})
}
// A dangling symlink still marks the service as added to the runlevel, so
// Lstat (not Stat) is what openrcOnIn must use.
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, "etc/runlevels/default"), 0o755))
require.NoError(t, os.Symlink("/nonexistent",
filepath.Join(root, "etc/runlevels/default/dangling")))
assert.True(t, openrcOnIn(root, "dangling"))
assert.False(t, openrcOnIn(t.TempDir(), "net.lo"))
}
// Every service name detection asks systemd about must be in networkServiceUnits,
// since that list is what the unit-file lookup is filtered by. A name missing
// from it silently loses its systemd answer.
func TestNetworkServiceUnitsCoversDetectedServices(t *testing.T) {
for _, name := range []string{"systemd-networkd", "NetworkManager", "network", "networking"} {
assert.Contains(t, networkServiceUnits, name)
}
}