internal/server/store/allocation_test.go
Ref: Size: 6.5 KiB History
package store
import (
"testing"
"github.com/a73x/eitri/internal/random"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func vmWithResources(t *testing.T, s *Store, h Host, name string, vcpus, mem, disk int64) VM {
t.Helper()
vm := VM{
ID: random.Hex(8), HostID: h.ID, Name: name,
ImageURL: "http://img", ImageSHA256: "abc",
VCPUs: vcpus, MemMB: mem, DiskGB: disk, PowerState: "running",
}
require.NoError(t, s.CreateVM(vm))
return vm
}
func TestAllocatedByHostSumsLiveVMs(t *testing.T) {
s := newStore(t)
h := enrollHost(t, s)
vmWithResources(t, s, h, "a", 2, 2048, 10)
vmWithResources(t, s, h, "b", 1, 1024, 5)
alloc := s.mustAllocated(t)
got := alloc[h.ID]
assert.Equal(t, int64(3), got.VCPUs)
assert.Equal(t, int64(3072), got.MemMB)
assert.Equal(t, int64(15), got.DiskGB)
}
func TestAllocatedByHostExcludesTombstoned(t *testing.T) {
s := newStore(t)
h := enrollHost(t, s)
vmWithResources(t, s, h, "a", 2, 2048, 10)
dead := vmWithResources(t, s, h, "b", 4, 4096, 20)
require.NoError(t, s.TombstoneVM(dead.ID))
got := s.mustAllocated(t)[h.ID]
assert.Equal(t, int64(2), got.VCPUs, "tombstoned VM must not count as allocated")
assert.Equal(t, int64(2048), got.MemMB)
assert.Equal(t, int64(10), got.DiskGB)
}
func TestAllocatedByHostSeparatesHosts(t *testing.T) {
s := newStore(t)
h1 := enrollHost(t, s)
tok, _ := s.CreateEnrollmentToken(testTenant)
h2, _ := s.RedeemEnrollmentToken(tok, EnrollFacts{Name: "h2", OS: "linux", Arch: "amd64", Provisioner: "cloudhv", Remote: ""})
vmWithResources(t, s, h1, "a", 2, 2048, 10)
vmWithResources(t, s, h2, "b", 8, 8192, 40)
alloc := s.mustAllocated(t)
assert.Equal(t, int64(2), alloc[h1.ID].VCPUs)
assert.Equal(t, int64(8), alloc[h2.ID].VCPUs)
}
// TestCommittedOnHostCountsTombstonesUntilTheyAreReaped is the other half of
// the pair above, and the difference between them is the point. What the
// console calls "allocated" is what is live; what a placement has to beat is
// what the machine is still holding, and a tombstoned VM's disk is on the host
// until the reap deletes the row.
func TestCommittedOnHostCountsTombstonesUntilTheyAreReaped(t *testing.T) {
s := newStore(t)
h := enrollHost(t, s)
vmWithResources(t, s, h, "a", 2, 2048, 10)
dying := vmWithResources(t, s, h, "b", 4, 4096, 20)
require.NoError(t, s.TombstoneVM(dying.ID))
held, err := s.CommittedOnHost(h.ID)
require.NoError(t, err)
assert.Equal(t, Alloc{VCPUs: 6, MemMB: 6144, DiskGB: 30}, held.Held(),
"a VM being destroyed still occupies the host")
require.NoError(t, s.HardDeleteVM(dying.ID, h.ID))
held, err = s.CommittedOnHost(h.ID)
require.NoError(t, err)
assert.Equal(t, Alloc{VCPUs: 2, MemMB: 2048, DiskGB: 10}, held.Held(),
"the reap is what frees it")
}
// TestCommittedOnHostSplitsWhatIsRunningFromWhatIsLeaving is the half of the
// commitment a refusal has to be able to name. Both halves are held, and only
// one of them can be freed by deleting something — the other frees itself.
func TestCommittedOnHostSplitsWhatIsRunningFromWhatIsLeaving(t *testing.T) {
s := newStore(t)
h := enrollHost(t, s)
vmWithResources(t, s, h, "a", 2, 2048, 10)
for _, name := range []string{"b", "c"} {
dying := vmWithResources(t, s, h, name, 4, 4096, 20)
require.NoError(t, s.TombstoneVM(dying.ID))
}
held, err := s.CommittedOnHost(h.ID)
require.NoError(t, err)
assert.Equal(t, Alloc{VCPUs: 2, MemMB: 2048, DiskGB: 10}, held.Live)
assert.Equal(t, Alloc{VCPUs: 8, MemMB: 8192, DiskGB: 40}, held.Pending)
assert.Equal(t, 2, held.PendingVMs, "the count is what a refusal names")
}
// TestCommittedOnHostSeparatesHosts: one host's commitment says nothing about
// another's, and a host with nothing on it holds nothing rather than erroring.
func TestCommittedOnHostSeparatesHosts(t *testing.T) {
s := newStore(t)
h := enrollHost(t, s)
vmWithResources(t, s, h, "a", 2, 2048, 10)
held, err := s.CommittedOnHost("no-such-host")
require.NoError(t, err)
assert.Equal(t, Commitment{}, held)
}
func (s *Store) mustAllocated(t *testing.T) map[string]Alloc {
t.Helper()
_, alloc, _, err := s.Snapshot()
require.NoError(t, err)
return alloc
}
// TestEnrollHostProposesTheFleetSuggests pins the inversion at the moment a
// host joins. The three states are deliberately distinct, which is why the
// field is a pointer: no opinion takes the fleet's suggestion, a stated subnet
// is recorded as given, and an explicit none is left alone for a host that will
// report its own once it can see it.
func TestEnrollHostProposesTheFleetSuggests(t *testing.T) {
cidr := func(s string) *string { return &s }
for _, tc := range []struct {
name string
proposal *string
want string // "" = expect the pool's allocation
wantErr string
}{
{name: "no opinion takes the suggestion", proposal: nil, want: "10.77.1.0/24"},
{name: "a stated subnet is recorded as given", proposal: cidr("192.168.64.0/24"), want: "192.168.64.0/24"},
{name: "an explicit none is left alone", proposal: cidr(""), want: ""},
{name: "unparseable is refused", proposal: cidr("not-a-network"), wantErr: "guest cidr"},
{name: "a bare address is refused", proposal: cidr("192.168.64.1"), wantErr: "guest cidr"},
{name: "IPv6 is refused", proposal: cidr("fd00::/64"), wantErr: "must be IPv4"},
} {
t.Run(tc.name, func(t *testing.T) {
s := newStore(t)
tok, err := s.CreateEnrollmentToken(testTenant)
require.NoError(t, err)
h, err := s.RedeemEnrollmentToken(tok, EnrollFacts{
Name: "h", OS: "linux", Arch: "amd64", Provisioner: "cloudhv",
BridgeCIDR: tc.proposal,
})
if tc.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tc.want, h.BridgeCIDR)
})
}
}
// TestEnrollWithAProposalDoesNotSpendAnAllocation pins that a host stating its
// own subnet leaves the pool where it was — the fleet allocates for hosts that
// want advice, not for every host that joins.
func TestEnrollWithAProposalDoesNotSpendAnAllocation(t *testing.T) {
s := newStore(t)
own := "192.168.64.0/24"
tok, _ := s.CreateEnrollmentToken(testTenant)
_, err := s.RedeemEnrollmentToken(tok, EnrollFacts{
Name: "mac", OS: "darwin", Arch: "arm64", Provisioner: "vfkit", BridgeCIDR: &own})
require.NoError(t, err)
tok2, _ := s.CreateEnrollmentToken(testTenant)
linux, err := s.RedeemEnrollmentToken(tok2, EnrollFacts{
Name: "linux", OS: "linux", Arch: "amd64", Provisioner: "cloudhv"})
require.NoError(t, err)
assert.Equal(t, "10.77.1.0/24", linux.BridgeCIDR,
"the first allocation is still available: the Mac consumed none")
}