internal/server/syncsvc/network_test.go
Ref: Size: 8.0 KiB History
package syncsvc
import (
"testing"
"time"
"github.com/a73x/eitri/internal/pb"
"github.com/a73x/eitri/internal/server/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestHelloAdvertisesHostNetworks is the whole basis of named-network
// admission: a create is only allowed onto a host that says it serves the
// network, and the only place a host ever says so is its Hello. If this stops
// reaching the registry, every bridged create on the fleet is refused.
func TestHelloAdvertisesHostNetworks(t *testing.T) {
f := setup(t)
_, err := dialHello(t, f.addr, f.fp, &pb.Hello{
HostId: f.host.ID, Provisioner: "cloudhv", Credential: f.cred,
HostNetworks: []string{"lan", "lab"},
})
require.NoError(t, err)
require.Eventually(t, func() bool {
st, ok := f.reg.Get(f.host.ID)
return ok && len(st.HostNetworks) == 2
}, 2*time.Second, 20*time.Millisecond, "the Hello's networks must reach the registry")
st, _ := f.reg.Get(f.host.ID)
assert.Equal(t, []string{"lan", "lab"}, st.HostNetworks)
}
// TestSnapshotCarriesTheVMsNetwork: the row records which underlay a guest was
// admitted onto, and the snapshot is the only way that reaches the agent that
// builds it. A NAT guest carries the empty name — the agent's default path —
// and neither one is inferred anywhere downstream.
func TestSnapshotCarriesTheVMsNetwork(t *testing.T) {
f := setup(t)
require.NoError(t, f.st.CreateVM(store.VM{
ID: "vm1", HostID: f.host.ID, Name: "bridged", ImageURL: "u", ImageSHA256: "s",
VCPUs: 1, MemMB: 512, DiskGB: 5, PowerState: "running", Network: "lan",
}))
require.NoError(t, f.st.CreateVM(store.VM{
ID: "vm2", HostID: f.host.ID, Name: "natted", ImageURL: "u", ImageSHA256: "s",
VCPUs: 1, MemMB: 512, DiskGB: 5, PowerState: "running",
}))
snap, err := f.svc.buildSnapshot(f.host.ID)
require.NoError(t, err)
got := map[string]string{}
for _, v := range snap.GetVms() {
got[v.GetVmId()] = v.GetNetwork()
}
assert.Equal(t, map[string]string{"vm1": "lan", "vm2": ""}, got)
}
// networkedVM creates one VM on the named network `lan` for the report tests.
func networkedVM(t *testing.T, f *fixture, id string) {
t.Helper()
require.NoError(t, f.st.CreateVM(store.VM{
ID: id, HostID: f.host.ID, Name: id, ImageURL: "u", ImageSHA256: "s",
VCPUs: 1, MemMB: 512, DiskGB: 5, PowerState: "running", Network: "lan",
}))
}
// TestApplyReportRecordsBothAddresses pins the inbound leg of the second NIC:
// the address the site's DHCP granted is durable beside — never instead of —
// the private-fabric address the gate and every exposure aim at.
func TestApplyReportRecordsBothAddresses(t *testing.T) {
f := setup(t)
networkedVM(t, f, "vm1")
f.svc.applyReport(f.host.ID, &pb.Report{
Vms: []*pb.VMStatus{{
VmId: "vm1", PowerState: "running", Phase: "ready",
Ip: "10.77.1.2", NetworkIp: "192.168.0.42",
}},
})
vm, err := f.st.GetVM("vm1")
require.NoError(t, err)
assert.Equal(t, "10.77.1.2", vm.AssignedIP)
assert.Equal(t, "192.168.0.42", vm.NetworkIP)
}
// TestApplyReportRecordsTheLeaseBeforeTheGuestIsReady: the site's DHCP answers
// on its own clock, which is not the lifecycle's. A lease that lands while the
// guest is still booting is recorded then — the status write's ready/failed
// filter must not hold the address hostage.
func TestApplyReportRecordsTheLeaseBeforeTheGuestIsReady(t *testing.T) {
f := setup(t)
networkedVM(t, f, "vm1")
f.svc.applyReport(f.host.ID, &pb.Report{
Vms: []*pb.VMStatus{{VmId: "vm1", PowerState: "running", Phase: "creating", NetworkIp: "192.168.0.42"}},
})
vm, err := f.st.GetVM("vm1")
require.NoError(t, err)
assert.Equal(t, "192.168.0.42", vm.NetworkIP)
assert.Equal(t, "pending", vm.Status, "the lifecycle write still waits for ready or failed")
}
// TestApplyReportKeepsTheLastKnownLease: an agent restarted before its guest
// renews reports no address on the named NIC, and silence is not "the guest
// left the network". The stored address stands until a new one replaces it.
func TestApplyReportKeepsTheLastKnownLease(t *testing.T) {
f := setup(t)
networkedVM(t, f, "vm1")
ready := func(netIP string) *pb.Report {
return &pb.Report{Vms: []*pb.VMStatus{{
VmId: "vm1", PowerState: "running", Phase: "ready", Ip: "10.77.1.2", NetworkIp: netIP,
}}}
}
f.svc.applyReport(f.host.ID, ready("192.168.0.42"))
f.svc.applyReport(f.host.ID, ready(""))
vm, err := f.st.GetVM("vm1")
require.NoError(t, err)
assert.Equal(t, "192.168.0.42", vm.NetworkIP, "an empty report is 'not known', not 'gone'")
// A renewal onto a different address is followed, not stuck.
f.svc.applyReport(f.host.ID, ready("192.168.0.77"))
vm, err = f.st.GetVM("vm1")
require.NoError(t, err)
assert.Equal(t, "192.168.0.77", vm.NetworkIP)
// An unusable address (what a guest shows when nothing answered) is one
// the store drops and keeps the prior value for — the cache still commits
// the raw report, so this proves the divergence between what netIPTrack
// remembers and what the row holds is harmless: the next GOOD address
// still reads as a change against the cache and lands.
f.svc.applyReport(f.host.ID, ready("169.254.11.2"))
vm, err = f.st.GetVM("vm1")
require.NoError(t, err)
assert.Equal(t, "192.168.0.77", vm.NetworkIP, "an unusable address is dropped, not stored")
f.svc.applyReport(f.host.ID, ready("192.168.0.99"))
vm, err = f.st.GetVM("vm1")
require.NoError(t, err)
assert.Equal(t, "192.168.0.99", vm.NetworkIP, "a good address after junk still lands")
}
// TestApplyReportRoutesLeasesThroughTheDedupCache pins the half of the skip
// that lives in applyReport: every reported address goes through netIPTrack,
// keyed by VM. A bridged guest re-reports the same lease every tick forever and
// the store runs on one connection, so a direct RecordVMNetworkIP here would
// spend a round trip per bridged guest per tick for the life of the fleet — and
// leave no trace, because the write succeeds every time.
//
// The skip itself — repeat writes nothing, change writes — belongs to
// TestNetTrackerWritesOnceThenStaysQuiet, which counts writes. This test can
// only see the cache, so it asserts what the cache proves: that applyReport
// consults it, with the reported address under the reporting VM's id.
func TestApplyReportRoutesLeasesThroughTheDedupCache(t *testing.T) {
f := setup(t)
networkedVM(t, f, "vm1")
report := func(ip string) *pb.Report {
return &pb.Report{Vms: []*pb.VMStatus{{
VmId: "vm1", PowerState: "running", Phase: "ready", Ip: "10.77.1.2", NetworkIp: ip,
}}}
}
f.svc.applyReport(f.host.ID, report("192.168.0.42"))
f.svc.applyReport(f.host.ID, report("192.168.0.42"))
f.svc.applyReport(f.host.ID, report("192.168.0.42"))
assertLeaseCache(t, f, map[string]string{"vm1": "192.168.0.42"},
"a lease that never reached the cache is a lease re-written every tick forever")
// A renewal to a different address must land in the cache, or the guest's
// new lease is remembered as the old one and never written again.
f.svc.applyReport(f.host.ID, report("192.168.0.99"))
assertLeaseCache(t, f, map[string]string{"vm1": "192.168.0.99"},
"the cache must hold the newest reported lease, not the first one seen")
}
// assertLeaseCache reads netIPTrack under its own lock and compares it to want.
func assertLeaseCache(t *testing.T, f *fixture, want map[string]string, msg string) {
t.Helper()
f.svc.netIPTrack.mu.Lock()
defer f.svc.netIPTrack.mu.Unlock()
assert.Equal(t, want, f.svc.netIPTrack.last, msg)
}
// TestApplyReportForgetsAReapedVMsLease bounds the cache: a VM whose row is
// hard-deleted must not leave its address remembered forever.
func TestApplyReportForgetsAReapedVMsLease(t *testing.T) {
f := setup(t)
networkedVM(t, f, "vm1")
f.svc.applyReport(f.host.ID, &pb.Report{
Vms: []*pb.VMStatus{{VmId: "vm1", PowerState: "running", Phase: "ready", NetworkIp: "192.168.0.42"}},
})
require.NoError(t, f.st.TombstoneVM("vm1"))
f.svc.applyReport(f.host.ID, &pb.Report{Destroyed: []string{"vm1"}})
f.svc.netIPTrack.mu.Lock()
defer f.svc.netIPTrack.mu.Unlock()
assert.Empty(t, f.svc.netIPTrack.last, "a reaped VM's address is dead weight")
}