internal/server/syncsvc/volumes_test.go
Ref: Size: 6.4 KiB History
package syncsvc
import (
"testing"
"github.com/a73x/eitri/internal/pb"
"github.com/a73x/eitri/internal/server/registry"
"github.com/a73x/eitri/internal/server/release"
"github.com/a73x/eitri/internal/server/store"
"github.com/stretchr/testify/require"
)
// claimedVM gives the host one VM holding one 4GiB claim, and returns the
// claim as the store now reads it — bound to a volume, held by the VM.
func claimedVM(t *testing.T, st *store.Store, hostID, vmID string) store.VolumeClaim {
t.Helper()
c, err := st.CreateVolumeClaim(testTenant, vmID+"-data", 4)
require.NoError(t, err)
require.NoError(t, st.CreateVM(store.VM{ID: vmID, HostID: hostID, Name: vmID, ImageURL: "u", ImageSHA256: "s",
VCPUs: 1, MemMB: 1, DiskGB: 1, PowerState: "running", VolumeClaimIDs: []string{c.ID}}))
c, err = st.GetVolumeClaim(c.ID)
require.NoError(t, err)
return c
}
func TestSnapshotCarriesVolumesAndFloor(t *testing.T) {
f := setup(t)
c := claimedVM(t, f.st, f.host.ID, "vm1")
snap, err := f.svc.buildSnapshot(f.host.ID)
require.NoError(t, err)
require.Len(t, snap.Volumes, 1)
require.Equal(t, c.BoundVolumeID, snap.Volumes[0].VolumeId)
require.EqualValues(t, 4, snap.Volumes[0].SizeGb)
require.False(t, snap.Volumes[0].Tombstoned)
require.Equal(t, []string{c.BoundVolumeID}, snap.Vms[0].VolumeIds)
require.Equal(t, release.Volumes.Since, snap.MinAgentVersion)
}
func TestSnapshotWithoutVolumesHasNoFloor(t *testing.T) {
f := setup(t)
snap, err := f.svc.buildSnapshot(f.host.ID)
require.NoError(t, err)
require.Equal(t, "", snap.MinAgentVersion, "a pre-volumes agent must keep taking ordinary snapshots")
}
// tombstoneFreeClaim gets the claim to the only state a reap may act on: the
// VM that held it destroyed, the claim itself tombstoned.
func tombstoneFreeClaim(t *testing.T, st *store.Store, c store.VolumeClaim) {
t.Helper()
require.NoError(t, st.TombstoneVM(c.VMID))
require.NoError(t, st.HardDeleteVM(c.VMID, c.HostID))
require.NoError(t, st.TombstoneVolumeClaim(c.ID))
}
func TestTombstonedVolumeIsReapedWhenHostReportsItGone(t *testing.T) {
f := setup(t)
f.reg.SetAgentVersion(f.host.ID, release.Volumes.Since)
c := claimedVM(t, f.st, f.host.ID, "vm1")
tombstoneFreeClaim(t, f.st, c)
f.svc.applyReport(f.host.ID, &pb.Report{Volumes: []*pb.VolumeStatus{{VolumeId: c.BoundVolumeID, Present: true, SizeGb: 4}}})
vols, err := f.st.ListVolumesForHost(f.host.ID)
require.NoError(t, err)
require.Len(t, vols, 1, "the host still has the file")
f.svc.applyReport(f.host.ID, &pb.Report{Volumes: []*pb.VolumeStatus{{VolumeId: c.BoundVolumeID, Present: false}}})
vols, err = f.st.ListVolumesForHost(f.host.ID)
require.NoError(t, err)
require.Empty(t, vols, "reported gone: row reaped")
// The claim goes with it, so its name is free again.
_, err = f.st.GetVolumeClaim(c.ID)
require.Error(t, err)
}
// A live volume the host does not list is NOT reaped: the row is the truth
// the agent converges toward, and absence on one tick is a file not yet made.
func TestLiveVolumeSurvivesAnEmptyReport(t *testing.T) {
f := setup(t)
f.reg.SetAgentVersion(f.host.ID, release.Volumes.Since)
claimedVM(t, f.st, f.host.ID, "vm1")
f.svc.applyReport(f.host.ID, &pb.Report{})
vols, err := f.st.ListVolumesForHost(f.host.ID)
require.NoError(t, err)
require.Len(t, vols, 1)
}
// Even told outright the file is gone, a live volume stays: the tenant still
// holds the claim, and the agent's next converge is what makes the file again.
func TestLiveVolumeSurvivesAPresentFalseReport(t *testing.T) {
f := setup(t)
f.reg.SetAgentVersion(f.host.ID, release.Volumes.Since)
c := claimedVM(t, f.st, f.host.ID, "vm1")
f.svc.applyReport(f.host.ID, &pb.Report{Volumes: []*pb.VolumeStatus{{VolumeId: c.BoundVolumeID, Present: false}}})
vols, err := f.st.ListVolumesForHost(f.host.ID)
require.NoError(t, err)
require.Len(t, vols, 1)
}
// A pre-volumes agent never reports volumes, so its silence proves nothing
// about the file; the tombstoned row waits for an agent that can answer.
func TestTombstonedVolumeIsNotReapedOnAPreVolumesAgentReport(t *testing.T) {
f := setup(t)
f.reg.SetAgentVersion(f.host.ID, "v0.0.6")
c := claimedVM(t, f.st, f.host.ID, "vm1")
tombstoneFreeClaim(t, f.st, c)
f.svc.applyReport(f.host.ID, &pb.Report{})
vols, err := f.st.ListVolumesForHost(f.host.ID)
require.NoError(t, err)
require.Len(t, vols, 1)
}
// A fenced report is the agent refusing a stale snapshot: it acted on nothing,
// so nothing in it answers what the snapshot asked for. Reaping off it would
// delete every tombstoned row on the host for a report that never looked.
func TestTombstonedVolumeIsNotReapedOnAFencedReport(t *testing.T) {
f := setup(t)
f.reg.SetAgentVersion(f.host.ID, release.Volumes.Since)
c := claimedVM(t, f.st, f.host.ID, "vm1")
tombstoneFreeClaim(t, f.st, c)
f.svc.applyReport(f.host.ID, &pb.Report{FenceViolation: true})
vols, err := f.st.ListVolumesForHost(f.host.ID)
require.NoError(t, err)
require.Len(t, vols, 1, "an omission in a fenced report is not a report that the file is gone")
// The same omission from an accepted report still reaps: the guard is the
// fence, not a new reluctance.
f.svc.applyReport(f.host.ID, &pb.Report{})
vols, err = f.st.ListVolumesForHost(f.host.ID)
require.NoError(t, err)
require.Empty(t, vols)
}
// The reap is a tenant event, not a system one: the claim is gone by the time
// it lands, so the tenant is read from the tombstoned row.
func TestReapIsAuditedUnderTheClaimsTenant(t *testing.T) {
f := setup(t)
f.reg.SetAgentVersion(f.host.ID, release.Volumes.Since)
c := claimedVM(t, f.st, f.host.ID, "vm1")
tombstoneFreeClaim(t, f.st, c)
f.svc.applyReport(f.host.ID, &pb.Report{Volumes: []*pb.VolumeStatus{{VolumeId: c.BoundVolumeID, Present: false}}})
events, err := f.st.ListAudit(testTenant, 50)
require.NoError(t, err)
var found bool
for _, e := range events {
if e.Action == "volume.reap" {
found = true
require.Contains(t, e.Detail, c.BoundVolumeID)
}
}
require.True(t, found, "volume.reap audited under %s", testTenant)
}
// The report reaches the registry, so the console can say what the host found.
func TestReportedVolumesReachTheRegistry(t *testing.T) {
f := setup(t)
c := claimedVM(t, f.st, f.host.ID, "vm1")
f.svc.applyReport(f.host.ID, &pb.Report{Volumes: []*pb.VolumeStatus{{VolumeId: c.BoundVolumeID, Present: true, SizeGb: 4}}})
hs, ok := f.reg.Get(f.host.ID)
require.True(t, ok)
require.Equal(t, []registry.VolumeStatus{{VolumeID: c.BoundVolumeID, Present: true, SizeGB: 4}}, hs.Volumes)
}