168 lines
6.3 KiB
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)
|
|
}
|
|
}
|