659 lines
22 KiB
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
|
|
}
|