internal/agent/vfkit/leases_test.go
Ref: Size: 5.7 KiB History
package vfkit
import (
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/a73x/eitri/internal/agent/state"
)
// lease renders one dhcpd_leases stanza in macOS's format: bootpd writes octets
// unpadded, prefixes the ARP hardware type, and stamps the expiry epoch as hex.
func lease(name, ip, mac string, expiry uint64) string {
return "{\n\tname=" + name + "\n\tip_address=" + ip + "\n\thw_address=1," + mac +
"\n\tidentifier=1," + mac + "\n\tlease=0x" + strconv.FormatUint(expiry, 16) + "\n}\n"
}
// Two expiries a day apart, so which one a test means is never in doubt.
const (
older = 0x68a1b2c3
newer = 0x68a30443
)
func TestLeaseAddressFindsTheGuestByItsMAC(t *testing.T) {
// state.MAC pads every octet; bootpd does not. Matching the two as written
// would miss every lease this backend ever needs to read.
const padded = "52:54:00:0a:0b:0c"
leases := lease("other", "192.168.64.2", "52:54:0:ff:ee:dd", newer) +
lease("web", "192.168.64.7", "52:54:0:a:b:c", newer)
assert.Equal(t, "192.168.64.7", leaseAddress(leases, padded))
}
func TestLeaseAddressIgnoresALeaseKeyedByADUID(t *testing.T) {
const mac = "52:54:00:0a:0b:0c"
// A guest that identifies itself by a DUID gets its lease filed under one:
// hardware type "ff" and an opaque identifier that is not six octets. Such
// a stanza is not a candidate for any VM, so it can neither be returned nor
// displace a real match — which is the whole reason the one lease database
// captured on real hardware had two stanzas for one guest and still gave
// one unambiguous answer. The seed stops the guest asking that way at all;
// this is the parser's half of the same guarantee.
duid := "{\n\tname=web\n\tip_address=192.168.64.8" +
"\n\thw_address=ff,f1:f5:dd:7f:0:2:0:0:ab:11:69:49:e9:aa:ed:20:59:eb" +
"\n\tidentifier=ff,f1:f5:dd:7f:0:2:0:0:ab:11:69:49:e9:aa:ed:20:59:eb\n\tlease=0x6a6e5628\n}\n"
mine := lease("web", "192.168.64.7", "52:54:0:a:b:c", newer)
assert.Equal(t, "192.168.64.7", leaseAddress(duid+mine, mac))
assert.Equal(t, "192.168.64.7", leaseAddress(mine+duid, mac))
assert.Empty(t, leaseAddress(duid, mac), "a DUID lease belongs to no VM this backend knows")
}
func TestLeaseAddressTakesTheLastOfTwoCandidates(t *testing.T) {
const mac = "52:54:00:0a:0b:0c"
// Two stanzas keyed on one MAC should not happen — the guest asks under one
// address and the seed pins its client identifier to it. Nothing in the file
// ranks them if they do (`lease` is an expiry, not a grant time), so the
// rule is arbitrary and pinned here only so that changing it is deliberate.
leases := lease("web", "192.168.64.7", "52:54:0:a:b:c", older) +
lease("web", "192.168.64.9", "52:54:0:a:b:c", newer)
assert.Equal(t, "192.168.64.9", leaseAddress(leases, mac))
}
func TestLeaseAddressIsEmptyWhenTheGuestHasNotAskedYet(t *testing.T) {
leases := lease("other", "192.168.64.2", "52:54:0:ff:ee:dd", newer)
// Empty means "no answer", never "no address" — reconcile leaves any
// recorded address alone rather than clearing it.
assert.Empty(t, leaseAddress(leases, "52:54:00:0a:0b:0c"))
}
func TestLeaseAddressIgnoresAStanzaWithNoAddress(t *testing.T) {
leases := "{\n\tname=web\n\thw_address=1,52:54:0:a:b:c\n}\n"
assert.Empty(t, leaseAddress(leases, "52:54:00:0a:0b:0c"))
}
func TestLeaseAddressSurvivesAMalformedFile(t *testing.T) {
for name, leases := range map[string]string{
"empty": "",
"no braces": "ip_address=192.168.64.7\nhw_address=1,52:54:0:a:b:c\n",
"unterminated": "{\n\tip_address=192.168.64.7\n\thw_address=1,52:54:0:a:b:c\n",
"junk hw_address": lease("web", "192.168.64.7", "not-a-mac", newer),
"truncated octets": lease("web", "192.168.64.7", "52:54:0:a:b", newer),
// hw_address is checked exhaustively; ip_address must be too, or a
// stanza carrying our MAC and junk becomes the address we dial.
"junk ip_address": lease("web", "not-an-address", "52:54:0:a:b:c", newer),
"empty ip_address": lease("web", "", "52:54:0:a:b:c", newer),
"ip with a port": lease("web", "192.168.64.7:22", "52:54:0:a:b:c", newer),
} {
t.Run(name, func(t *testing.T) {
// The lease database is written by a system daemon we do not
// control; nothing in it may make the agent report a wrong address.
assert.Empty(t, leaseAddress(leases, "52:54:00:0a:0b:0c"))
})
}
}
func TestNormalizeMACRejectsAnythingThatIsNotSixOctets(t *testing.T) {
for _, in := range []string{"", "52:54:00", "52:54:00:0a:0b:0c:0d", "52-54-00-0a-0b-0c", "1,", "gg:54:00:0a:0b:0c"} {
assert.Empty(t, normalizeMAC(in), "%q must not normalise to anything a real MAC could equal", in)
}
// An octet past a byte is not a MAC either, however hex-looking it is.
assert.Empty(t, normalizeMAC("152:54:00:0a:0b:0c"))
}
func TestAddressReadsTheHostLeaseDatabase(t *testing.T) {
p := newTestProv(t, nil)
require.NoError(t, os.WriteFile(p.leasesPath,
[]byte(lease("web", "192.168.64.7", trimMACZeros(state.MAC("vm-1")), newer)), 0o600))
assert.Equal(t, "192.168.64.7", p.Address("vm-1"))
}
func TestAddressIsEmptyBeforeAnyGuestHasLeased(t *testing.T) {
p := newTestProv(t, nil)
p.leasesPath = filepath.Join(t.TempDir(), "never-written")
// A Mac that has never run a guest has no lease database at all; that is
// "no answer", not a failure worth reporting up the seam.
assert.Empty(t, p.Address("vm-1"))
}
// trimMACZeros rewrites a padded MAC the way bootpd would write it, so the test
// fixture is the file's format rather than ours.
func trimMACZeros(mac string) string {
octets := strings.Split(mac, ":")
for i, o := range octets {
octets[i] = strings.TrimLeft(o, "0")
if octets[i] == "" {
octets[i] = "0"
}
}
return strings.Join(octets, ":")
}