a73x

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")
}