go-network-configurator/dhcp_unit_test.go

659 lines
22 KiB
Go

package netconfig
import (
"context"
"net"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/ini.v1"
)
// cidr is a terse *net.IPNet literal for the tests below.
func cidr(t *testing.T, s string) *net.IPNet {
t.Helper()
ip, ipnet, err := net.ParseCIDR(s)
require.NoError(t, err)
ipnet.IP = ip
return ipnet
}
// A netplan interface must report and record each family's client
// independently, and must not grow a dhcp4/dhcp6 key that was never there.
func TestNetplanSetDHCP(t *testing.T) {
t.Run("reads absent keys as off", func(t *testing.T) {
dhcp4, dhcp6 := (&npInterface{}).dhcpState()
assert.False(t, dhcp4)
assert.False(t, dhcp6)
})
t.Run("enables one family without touching the other", func(t *testing.T) {
c := npInterface{}
c.setDHCP(true, false)
require.NotNil(t, c.DHCP4)
assert.True(t, *c.DHCP4)
assert.Nil(t, c.DHCP6, "dhcp6 was absent and stays absent when disabled")
})
t.Run("disabling drops that family's overrides only", func(t *testing.T) {
c := npInterface{
DHCP4: boolPtr(true),
DHCP6: boolPtr(true),
DHCP4Overrides: map[string]any{"use-dns": false},
DHCP6Overrides: map[string]any{"use-dns": false},
}
c.setDHCP(false, true)
assert.False(t, *c.DHCP4)
assert.Nil(t, c.DHCP4Overrides, "overrides of a stopped client are dead config")
assert.True(t, *c.DHCP6)
assert.NotNil(t, c.DHCP6Overrides)
})
t.Run("disabling an absent key does not write it", func(t *testing.T) {
c := npInterface{}
c.setDHCP(false, false)
assert.Nil(t, c.DHCP4)
assert.Nil(t, c.DHCP6)
})
t.Run("round trips through dhcpState", func(t *testing.T) {
c := npInterface{}
c.setDHCP(true, true)
dhcp4, dhcp6 := c.dhcpState()
assert.True(t, dhcp4)
assert.True(t, dhcp6)
})
}
func TestCloudInitSetDHCP(t *testing.T) {
c := ciInterface{DHCP4: boolPtr(true), DHCP6Overrides: map[string]any{"use-dns": false}}
c.setDHCP(false, false)
require.NotNil(t, c.DHCP4)
assert.False(t, *c.DHCP4)
assert.Nil(t, c.DHCP6, "absent stays absent")
assert.Nil(t, c.DHCP6Overrides)
}
func TestParseNetworkdDHCP(t *testing.T) {
tests := []struct {
value string
dhcp4, dhcp6 bool
}{
{value: "yes", dhcp4: true, dhcp6: true},
{value: "true", dhcp4: true, dhcp6: true},
{value: "both", dhcp4: true, dhcp6: true},
{value: " YES ", dhcp4: true, dhcp6: true},
{value: "ipv4", dhcp4: true},
{value: "v4", dhcp4: true},
{value: "ipv6", dhcp6: true},
{value: "no"},
{value: "none"},
{value: ""},
{value: "garbage"},
}
for _, tt := range tests {
t.Run(tt.value, func(t *testing.T) {
dhcp4, dhcp6 := parseNetworkdDHCP(tt.value)
assert.Equal(t, tt.dhcp4, dhcp4, "dhcp4")
assert.Equal(t, tt.dhcp6, dhcp6, "dhcp6")
})
}
}
func TestFormatNetworkdDHCP(t *testing.T) {
tests := []struct {
name string
dhcp4, dhcp6 bool
value string
keep bool
}{
{name: "both", dhcp4: true, dhcp6: true, value: "yes", keep: true},
{name: "v4", dhcp4: true, value: "ipv4", keep: true},
{name: "v6", dhcp6: true, value: "ipv6", keep: true},
{name: "neither", value: "", keep: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
value, keep := formatNetworkdDHCP(tt.dhcp4, tt.dhcp6)
assert.Equal(t, tt.keep, keep)
assert.Equal(t, tt.value, value)
})
}
}
// networkdDHCPValue builds a [Network] section carrying the given DHCP= value
// ("" for no key at all), runs setNetworkdDHCP over it, and reports the
// resulting value plus whether the key survived.
func networkdDHCPValue(t *testing.T, start string, dhcp4, dhcp6 bool) (string, bool) {
t.Helper()
cfg := ini.Empty()
sec, err := cfg.NewSection("Network")
require.NoError(t, err)
if start != "" {
_, err = sec.NewKey("DHCP", start)
require.NoError(t, err)
}
require.NoError(t, setNetworkdDHCP(sec, dhcp4, dhcp6))
if !sec.HasKey("DHCP") {
return "", false
}
return sec.Key("DHCP").String(), true
}
func TestSetNetworkdDHCP(t *testing.T) {
t.Run("narrows a dual-stack lease to one family", func(t *testing.T) {
value, present := networkdDHCPValue(t, "yes", false, true)
require.True(t, present)
assert.Equal(t, "ipv6", value)
})
t.Run("removes the key when no family is left", func(t *testing.T) {
_, present := networkdDHCPValue(t, "ipv4", false, false)
assert.False(t, present, "DHCP= should be removed, not written as no")
})
t.Run("creates the key on a config that lacked it", func(t *testing.T) {
value, present := networkdDHCPValue(t, "", true, false)
require.True(t, present)
assert.Equal(t, "ipv4", value)
})
t.Run("leaves an absent key absent when disabling", func(t *testing.T) {
_, present := networkdDHCPValue(t, "", false, false)
assert.False(t, present)
})
}
// A .network file must round trip through SetIfaceDHCP and GetInterfaces, and
// SetIfaceAddresses must not disturb the DHCP key on the way past.
func TestNetworkdSetIfaceDHCP(t *testing.T) {
newNetworkdDir := func(t *testing.T, body string) (string, string) {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, "eth0.network")
require.NoError(t, os.WriteFile(path, []byte(body), 0644))
return dir, path
}
t.Run("enables a family and reads it back", func(t *testing.T) {
dir, path := newNetworkdDir(t, "[Match]\nName=eth0\n\n[Network]\nAddress=1.2.3.4/24\n")
nd, err := readNetworkdConfigDirectory(dir)
require.NoError(t, err)
require.NoError(t, nd.SetIfaceDHCP(context.Background(), "eth0", true, false))
cfg, err := ini.Load(path)
require.NoError(t, err)
assert.Equal(t, "ipv4", cfg.Section("Network").Key("DHCP").String())
assert.Equal(t, "1.2.3.4/24", cfg.Section("Network").Key("Address").String(),
"the static address must survive enabling DHCP")
})
t.Run("a DHCP-only network still reports its state", func(t *testing.T) {
dir, _ := newNetworkdDir(t, "[Match]\nName=eth0\n\n[Network]\nDHCP=yes\n")
nd, err := readNetworkdConfigDirectory(dir)
require.NoError(t, err)
ifaces, err := nd.GetInterfaces()
require.NoError(t, err)
require.Len(t, ifaces, 1)
assert.True(t, ifaces[0].DHCP4, "a network with no Address must still report DHCP")
assert.True(t, ifaces[0].DHCP6)
})
t.Run("setting addresses leaves DHCP alone", func(t *testing.T) {
dir, path := newNetworkdDir(t, "[Match]\nName=eth0\n\n[Network]\nDHCP=yes\n")
nd, err := readNetworkdConfigDirectory(dir)
require.NoError(t, err)
err = nd.SetIfaceAddresses(context.Background(), "eth0",
[]*net.IPNet{cidr(t, "1.2.3.4/24")}, net.ParseIP("1.2.3.1"), nil)
require.NoError(t, err)
cfg, err := ini.Load(path)
require.NoError(t, err)
assert.Equal(t, "yes", cfg.Section("Network").Key("DHCP").String(),
"adding a static address must not disable DHCP")
assert.Equal(t, "1.2.3.4/24", cfg.Section("Network").Key("Address").String())
})
}
func TestNSDHCPState(t *testing.T) {
tests := []struct {
name string
config map[string]string
dhcp4, dhcp6 bool
}{
{name: "empty", config: map[string]string{}},
{name: "static", config: map[string]string{"BOOTPROTO": "none"}},
{name: "dhcp", config: map[string]string{"BOOTPROTO": "dhcp"}, dhcp4: true},
{name: "uppercase dhcp", config: map[string]string{"BOOTPROTO": "DHCP"}, dhcp4: true},
{name: "bootp is dynamic", config: map[string]string{"BOOTPROTO": "bootp"}, dhcp4: true},
{name: "dhcpv6c", config: map[string]string{"DHCPV6C": "yes"}, dhcp6: true},
{
name: "both",
config: map[string]string{"BOOTPROTO": "dhcp", "DHCPV6C": "yes"},
dhcp4: true, dhcp6: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dhcp4, dhcp6 := nsDHCPState(tt.config)
assert.Equal(t, tt.dhcp4, dhcp4, "dhcp4")
assert.Equal(t, tt.dhcp6, dhcp6, "dhcp6")
})
}
}
func TestNetworkScriptsSetIfaceDHCP(t *testing.T) {
// Re-read the ifcfg file from disk so we assert on what was persisted.
readConfig := func(t *testing.T, dir string) map[string]string {
t.Helper()
ns, err := newNetworkScriptsWithConfig(dir)
require.NoError(t, err)
require.Len(t, ns.Interfaces, 1)
require.Len(t, ns.Interfaces[0].IFFiles, 1)
return ns.Interfaces[0].IFFiles[0].Config
}
newDir := func(t *testing.T, body string) string {
t.Helper()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "ifcfg-eth0"), []byte(body), 0644))
return dir
}
t.Run("enabling ipv4 keeps the static address", func(t *testing.T) {
dir := newDir(t, "DEVICE=eth0\nBOOTPROTO=none\nIPADDR0=1.2.3.4\nPREFIX0=24\n")
ns, err := newNetworkScriptsWithConfig(dir)
require.NoError(t, err)
require.NoError(t, ns.SetIfaceDHCP(context.Background(), "eth0", true, false))
config := readConfig(t, dir)
assert.Equal(t, "dhcp", config["BOOTPROTO"])
assert.Equal(t, "1.2.3.4", config["IPADDR0"], "the static address must survive")
})
t.Run("disabling ipv4 writes none and drops the dhclient option", func(t *testing.T) {
dir := newDir(t, "DEVICE=eth0\nBOOTPROTO=dhcp\nPERSISTENT_DHCLIENT=1\n")
ns, err := newNetworkScriptsWithConfig(dir)
require.NoError(t, err)
require.NoError(t, ns.SetIfaceDHCP(context.Background(), "eth0", false, false))
config := readConfig(t, dir)
assert.Equal(t, "none", config["BOOTPROTO"])
_, ok := config["PERSISTENT_DHCLIENT"]
assert.False(t, ok)
})
t.Run("the two families are independent", func(t *testing.T) {
dir := newDir(t, "DEVICE=eth0\nBOOTPROTO=dhcp\nDHCPV6C=yes\n")
ns, err := newNetworkScriptsWithConfig(dir)
require.NoError(t, err)
require.NoError(t, ns.SetIfaceDHCP(context.Background(), "eth0", false, true))
config := readConfig(t, dir)
assert.Equal(t, "none", config["BOOTPROTO"])
assert.Equal(t, "yes", config["DHCPV6C"])
assert.Equal(t, "yes", config["IPV6INIT"], "DHCPv6 needs IPv6 turned on")
})
t.Run("adding an address leaves BOOTPROTO alone", func(t *testing.T) {
dir := newDir(t, "DEVICE=eth0\nBOOTPROTO=dhcp\n")
ns, err := newNetworkScriptsWithConfig(dir)
require.NoError(t, err)
err = ns.SetIfaceAddresses(context.Background(), "eth0",
[]*net.IPNet{cidr(t, "1.2.3.4/24")}, nil, nil)
require.NoError(t, err)
config := readConfig(t, dir)
assert.Equal(t, "dhcp", config["BOOTPROTO"],
"a static address may coexist with a lease; only SetDHCP turns it off")
assert.Equal(t, "1.2.3.4", config["IPADDR0"])
})
t.Run("reads the state back through GetInterfaces", func(t *testing.T) {
dir := newDir(t, "DEVICE=eth0\nBOOTPROTO=dhcp\nDHCPV6C=yes\n")
ns, err := newNetworkScriptsWithConfig(dir)
require.NoError(t, err)
ifaces, err := ns.GetInterfaces()
require.NoError(t, err)
require.Len(t, ifaces, 1)
assert.True(t, ifaces[0].DHCP4)
assert.True(t, ifaces[0].DHCP6)
})
}
func TestIfUpDownSetDHCP(t *testing.T) {
t.Run("converts the stanza method for its own family", func(t *testing.T) {
iface := &ifInterface{Name: "eth0", Mode: "inet static"}
require.NoError(t, iface.setDHCP(true, false))
assert.Equal(t, "inet dhcp", iface.Mode)
assert.True(t, iface.isDHCP)
require.NoError(t, iface.setDHCP(false, false))
assert.Equal(t, "inet static", iface.Mode)
assert.False(t, iface.isDHCP)
})
t.Run("inet6 stanza follows dhcp6", func(t *testing.T) {
iface := &ifInterface{Name: "eth0", Mode: "inet6 static"}
require.NoError(t, iface.setDHCP(false, true))
assert.Equal(t, "inet6 dhcp", iface.Mode)
assert.True(t, iface.isDHCP)
})
t.Run("a stanza-less interface gets one", func(t *testing.T) {
iface := &ifInterface{Name: "eth0"}
require.NoError(t, iface.setDHCP(true, false))
assert.Equal(t, "inet dhcp", iface.Mode)
})
t.Run("errors rather than silently ignoring the other family", func(t *testing.T) {
iface := &ifInterface{Name: "eth0", Mode: "inet static"}
err := iface.setDHCP(false, true)
assert.ErrorContains(t, err, "other family")
assert.Equal(t, "inet static", iface.Mode, "the stanza must be untouched on error")
})
t.Run("errors on both families at once", func(t *testing.T) {
iface := &ifInterface{Name: "eth0"}
assert.ErrorContains(t, iface.setDHCP(true, true), "both families")
})
t.Run("refuses methods it cannot convert", func(t *testing.T) {
for _, method := range []string{"loopback", "ppp"} {
iface := &ifInterface{Name: "lo", Mode: "inet " + method}
assert.ErrorContains(t, iface.setDHCP(true, false), method)
}
})
t.Run("dhcpState reports the stanza's family only", func(t *testing.T) {
dhcp4, dhcp6 := (&ifInterface{Mode: "inet dhcp", isDHCP: true}).dhcpState()
assert.True(t, dhcp4)
assert.False(t, dhcp6)
dhcp4, dhcp6 = (&ifInterface{Mode: "inet6 dhcp", isDHCP: true}).dhcpState()
assert.False(t, dhcp4)
assert.True(t, dhcp6)
dhcp4, dhcp6 = (&ifInterface{Mode: "inet static"}).dhcpState()
assert.False(t, dhcp4)
assert.False(t, dhcp6)
})
}
// Switching an ifupdown interface to DHCP must drop the static addressing the
// stanza carried, since a "dhcp" stanza has no address option to hold it.
func TestIfUpDownSetIfaceDHCPPersists(t *testing.T) {
dir := t.TempDir()
cfgPath := filepath.Join(dir, "interfaces")
config := "auto eth0\niface eth0 inet static\n\taddress 1.2.3.4/24\n\tgateway 1.2.3.1\n"
require.NoError(t, os.WriteFile(cfgPath, []byte(config), 0644))
i := &ifUpDown{BaseConfig: cfgPath, BackupDir: filepath.Join(dir, "backup")}
require.NoError(t, os.Mkdir(i.BackupDir, 0755))
require.NoError(t, i.ReadFile(cfgPath))
require.NoError(t, i.SetIfaceDHCP(context.Background(), "eth0", true, false))
// Re-read from disk to confirm what was persisted.
reread := &ifUpDown{BaseConfig: cfgPath, BackupDir: i.BackupDir}
require.NoError(t, reread.ReadFile(cfgPath))
require.Len(t, reread.Interfaces, 1)
eth0 := reread.Interfaces[0]
assert.Equal(t, "inet dhcp", eth0.Mode)
assert.True(t, eth0.isDHCP)
assert.Empty(t, eth0.Addresses, "a dhcp stanza cannot carry a static address")
assert.Nil(t, eth0.Gateway4)
// And the interface reports itself as DHCPv4.
ifaces, err := reread.GetInterfaces()
require.NoError(t, err)
require.Len(t, ifaces, 1)
assert.True(t, ifaces[0].DHCP4)
assert.False(t, ifaces[0].DHCP6)
}
// SetIfaceAddresses must leave netplan's dhcp4 alone: a static address and a
// lease coexist, and only SetIfaceDHCP decides otherwise.
func TestNetplanSetIfaceAddressesKeepsDHCP(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "01-netcfg.yaml")
config := "network:\n version: 2\n ethernets:\n eth0:\n dhcp4: true\n"
require.NoError(t, os.WriteFile(path, []byte(config), 0644))
np, err := readNetplanConfigDirectory(dir)
require.NoError(t, err)
err = np.SetIfaceAddresses(context.Background(), "eth0",
[]*net.IPNet{cidr(t, "1.2.3.4/24")}, net.ParseIP("1.2.3.1"), nil)
require.NoError(t, err)
reread, err := readNetplanConfigDirectory(dir)
require.NoError(t, err)
require.Len(t, reread.configs, 1)
eth0, ok := reread.configs[0].Network.Ethernets["eth0"]
require.True(t, ok)
require.NotNil(t, eth0.DHCP4)
assert.True(t, *eth0.DHCP4, "adding a static address must not disable DHCP")
assert.Equal(t, []string{"1.2.3.4/24"}, eth0.Addresses)
}
// SetIfaceDHCP must write netplan's dhcp4/dhcp6 and leave the addresses alone.
func TestNetplanSetIfaceDHCPPersists(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "01-netcfg.yaml")
config := "network:\n version: 2\n ethernets:\n eth0:\n addresses:\n - 1.2.3.4/24\n"
require.NoError(t, os.WriteFile(path, []byte(config), 0644))
np, err := readNetplanConfigDirectory(dir)
require.NoError(t, err)
require.NoError(t, np.SetIfaceDHCP(context.Background(), "eth0", true, false))
reread, err := readNetplanConfigDirectory(dir)
require.NoError(t, err)
eth0, ok := reread.configs[0].Network.Ethernets["eth0"]
require.True(t, ok)
require.NotNil(t, eth0.DHCP4)
assert.True(t, *eth0.DHCP4)
assert.Nil(t, eth0.DHCP6, "the untouched family must not gain a key")
assert.Equal(t, []string{"1.2.3.4/24"}, eth0.Addresses,
"static addresses coexist with a lease")
// And it reads back through GetInterfaces.
ifaces, err := reread.GetInterfaces()
require.NoError(t, err)
require.Len(t, ifaces, 1)
assert.True(t, ifaces[0].DHCP4)
assert.False(t, ifaces[0].DHCP6)
}
// newTestCloudInit builds a cloudInit over a temporary network config file, so
// SetIfaceDHCP has somewhere real to Save to.
func newTestCloudInit(t *testing.T, config string) *cloudInit {
t.Helper()
dir := t.TempDir()
configPath := filepath.Join(dir, "network.yaml")
require.NoError(t, os.WriteFile(configPath, []byte(config), 0644))
ci, err := newCloudInitWith(configPath, filepath.Join(dir, "nothing.json"), filepath.Join(dir, "cloud.cfg.d"))
require.NoError(t, err)
return ci
}
// cloud-init's v1 schema encodes DHCP as a subnet type. Enabling a client must
// add that subnet without disturbing the static subnet beside it.
func TestCloudInitSetIfaceDHCPV1(t *testing.T) {
ci := newTestCloudInit(t, `network:
version: 1
config:
- type: physical
name: eth0
subnets:
- type: dhcp
- type: static
address: 1.2.3.4/24
gateway: 1.2.3.1
`)
// Turning IPv4 off and IPv6 on must replace the dhcp subnet with a dhcp6
// one and leave the static subnet in place.
require.NoError(t, ci.SetIfaceDHCP(context.Background(), "eth0", false, true))
subnets := ci.Network.Configs[0].Subnets
require.Len(t, subnets, 2)
assert.Equal(t, "dhcp6", subnets[0].Type)
assert.Equal(t, "static", subnets[1].Type)
assert.Equal(t, "1.2.3.4/24", subnets[1].Address)
ifaces, err := ci.GetInterfaces()
require.NoError(t, err)
require.Len(t, ifaces, 1)
assert.False(t, ifaces[0].DHCP4)
assert.True(t, ifaces[0].DHCP6)
}
// The v1 reader must recognise every spelling of a DHCP subnet.
func TestCloudInitV1DHCPSubnetTypes(t *testing.T) {
for _, tt := range []struct {
subnetType string
dhcp4, dhcp6 bool
}{
{subnetType: "dhcp", dhcp4: true},
{subnetType: "dhcp4", dhcp4: true},
{subnetType: "dhcp6", dhcp6: true},
{subnetType: "ipv6_dhcpv6-stateful", dhcp6: true},
{subnetType: "static"},
} {
t.Run(tt.subnetType, func(t *testing.T) {
ci := newTestCloudInit(t, `network:
version: 1
config:
- type: physical
name: eth0
subnets:
- type: `+tt.subnetType+`
address: 1.2.3.4/24
`)
ifaces, err := ci.GetInterfaces()
require.NoError(t, err)
require.Len(t, ifaces, 1)
assert.Equal(t, tt.dhcp4, ifaces[0].DHCP4, "dhcp4")
assert.Equal(t, tt.dhcp6, ifaces[0].DHCP6, "dhcp6")
})
}
}
// The v2 schema keeps DHCP in dhcp4/dhcp6 keys beside the static addresses.
func TestCloudInitSetIfaceDHCPV2(t *testing.T) {
ci := newTestCloudInit(t, `network:
version: 2
ethernets:
eth0:
addresses:
- 1.2.3.4/24
`)
require.NoError(t, ci.SetIfaceDHCP(context.Background(), "eth0", true, false))
eth0, ok := ci.Network.Ethernets["eth0"]
require.True(t, ok)
require.NotNil(t, eth0.DHCP4)
assert.True(t, *eth0.DHCP4)
assert.Nil(t, eth0.DHCP6, "the untouched family must not gain a key")
assert.Equal(t, []string{"1.2.3.4/24"}, eth0.Addresses)
}
func TestNMConnectionDHCPState(t *testing.T) {
tests := []struct {
name string
method4, method6 string
dhcp4, dhcp6 bool
}{
{name: "auto is dhcp", method4: "auto", method6: "auto", dhcp4: true, dhcp6: true},
{name: "manual is static", method4: "manual", method6: "manual"},
{name: "ipv6 dhcp method", method4: "manual", method6: "dhcp", dhcp6: true},
{name: "disabled", method4: "disabled", method6: "link-local"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conn := &nmConnection{Method4: tt.method4, Method6: tt.method6}
dhcp4, dhcp6 := conn.dhcpState()
assert.Equal(t, tt.dhcp4, dhcp4, "dhcp4")
assert.Equal(t, tt.dhcp6, dhcp6, "dhcp6")
})
}
}
// Turning a lease off must leave behind a method that still serves whatever
// static addresses the connection holds, and not strand IPv6 with no address.
func TestNMStaticMethod(t *testing.T) {
assert.Equal(t, "manual", nmStaticMethod4(true))
assert.Equal(t, "disabled", nmStaticMethod4(false))
assert.Equal(t, "manual", nmStaticMethod6(true))
assert.Equal(t, "link-local", nmStaticMethod6(false))
}
// applyIfaceDHCP reports whether any backend persisted the change, which is what
// SetDHCP relies on before asking the running system for a lease.
func TestApplyIfaceDHCP(t *testing.T) {
t.Run("reports true when one backend succeeds", func(t *testing.T) {
failing := &fakeIfaceBackend{err: assert.AnError}
ok := &fakeIfaceBackend{}
applied := applyIfaceDHCP(context.Background(), []namedIfaceBackend{
{name: "broken", backend: failing},
{name: "good", backend: ok},
}, "eth0", true, false)
assert.True(t, applied)
assert.Equal(t, 1, failing.dhcpCalls, "a failing backend must not stop the others")
assert.Equal(t, 1, ok.dhcpCalls)
assert.True(t, ok.lastDHCP4)
assert.False(t, ok.lastDHCP6)
})
t.Run("reports false when every backend fails", func(t *testing.T) {
applied := applyIfaceDHCP(context.Background(), []namedIfaceBackend{
{name: "broken", backend: &fakeIfaceBackend{err: assert.AnError}},
}, "eth0", true, true)
assert.False(t, applied)
})
t.Run("reports false with no backends at all", func(t *testing.T) {
assert.False(t, applyIfaceDHCP(context.Background(), nil, "eth0", true, true))
})
}
// renewDHCPOnBackends must skip backends that cannot reconfigure the running
// system (cloud-init) rather than erroring on them.
func TestRenewDHCPOnBackends(t *testing.T) {
renewer := &fakeRenewerBackend{}
backends := []namedIfaceBackend{
{name: "cloud-init", backend: &fakeIfaceBackend{}}, // no renewDHCP method
{name: "netplan", backend: renewer},
}
renewDHCPOnBackends(context.Background(), backends, "eth0")
assert.Equal(t, 1, renewer.renewCalls)
assert.Equal(t, "eth0", renewer.lastIface)
}
// fakeRenewerBackend is a file backend that can also reconfigure the running
// system, satisfying both ifaceBackend and dhcpRenewer.
type fakeRenewerBackend struct {
fakeIfaceBackend
renewCalls int
lastIface string
renewErr error
}
func (f *fakeRenewerBackend) renewDHCP(_ context.Context, iface string) error {
f.renewCalls++
f.lastIface = iface
return f.renewErr
}