Make iterating on a Firecracker-friendly Void guest practical without replacing the Debian default image path. Add local Void rootfs build/register/verify plumbing, a language-agnostic dev package baseline, and guest SSH/work-disk hardening so new images use the runtime bundle key, keep a normal root bash environment, and repair stale nested /root layouts on restart. Replace the guest PING/PONG responder with an HTTP /healthz agent over vsock, rename the runtime bundle and config surface from ping helper to agent while still accepting the legacy keys, and route the post-SSH reminder through the new vm.health path. Validated with GOCACHE=/tmp/banger-gocache go test ./..., make build, bash -n customize.sh make-rootfs-void.sh, and git diff --check.
450 lines
12 KiB
Go
450 lines
12 KiB
Go
package system
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type systemCall struct {
|
|
sudo bool
|
|
name string
|
|
args []string
|
|
}
|
|
|
|
type systemStep struct {
|
|
call systemCall
|
|
out []byte
|
|
err error
|
|
}
|
|
|
|
type scriptedRunner struct {
|
|
t *testing.T
|
|
steps []systemStep
|
|
calls []systemCall
|
|
}
|
|
|
|
func (r *scriptedRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
|
|
return r.next(systemCall{name: name, args: append([]string(nil), args...)})
|
|
}
|
|
|
|
func (r *scriptedRunner) RunSudo(ctx context.Context, args ...string) ([]byte, error) {
|
|
return r.next(systemCall{sudo: true, args: append([]string(nil), args...)})
|
|
}
|
|
|
|
func (r *scriptedRunner) next(call systemCall) ([]byte, error) {
|
|
r.t.Helper()
|
|
r.calls = append(r.calls, call)
|
|
if len(r.steps) == 0 {
|
|
r.t.Fatalf("unexpected call: %+v", call)
|
|
}
|
|
step := r.steps[0]
|
|
r.steps = r.steps[1:]
|
|
if step.call.sudo != call.sudo || step.call.name != call.name || !reflect.DeepEqual(step.call.args, call.args) {
|
|
r.t.Fatalf("call mismatch:\n got: %+v\n want: %+v", call, step.call)
|
|
}
|
|
return step.out, step.err
|
|
}
|
|
|
|
func (r *scriptedRunner) assertExhausted() {
|
|
r.t.Helper()
|
|
if len(r.steps) != 0 {
|
|
r.t.Fatalf("unconsumed steps: %+v", r.steps)
|
|
}
|
|
}
|
|
|
|
type funcRunner struct {
|
|
run func(ctx context.Context, name string, args ...string) ([]byte, error)
|
|
runSudo func(ctx context.Context, args ...string) ([]byte, error)
|
|
}
|
|
|
|
func (r funcRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
|
|
if r.run == nil {
|
|
return nil, errors.New("unexpected Run call")
|
|
}
|
|
return r.run(ctx, name, args...)
|
|
}
|
|
|
|
func (r funcRunner) RunSudo(ctx context.Context, args ...string) ([]byte, error) {
|
|
if r.runSudo == nil {
|
|
return nil, errors.New("unexpected RunSudo call")
|
|
}
|
|
return r.runSudo(ctx, args...)
|
|
}
|
|
|
|
func TestResizeExt4ImageStopsAtFirstFailure(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
setup func(t *testing.T) string
|
|
steps func(path string) []systemStep
|
|
wantErr string
|
|
wantCalls int
|
|
}{
|
|
{
|
|
name: "truncate failure",
|
|
setup: func(t *testing.T) string {
|
|
return t.TempDir()
|
|
},
|
|
wantErr: "",
|
|
wantCalls: 0,
|
|
},
|
|
{
|
|
name: "e2fsck failure",
|
|
steps: func(path string) []systemStep {
|
|
return []systemStep{
|
|
{call: systemCall{name: "e2fsck", args: []string{"-p", "-f", path}}, err: errors.New("e2fsck failed")},
|
|
}
|
|
},
|
|
wantErr: "e2fsck failed",
|
|
wantCalls: 1,
|
|
},
|
|
{
|
|
name: "resize2fs failure",
|
|
steps: func(path string) []systemStep {
|
|
return []systemStep{
|
|
{call: systemCall{name: "e2fsck", args: []string{"-p", "-f", path}}},
|
|
{call: systemCall{name: "resize2fs", args: []string{path}}, err: errors.New("resize2fs failed")},
|
|
}
|
|
},
|
|
wantErr: "resize2fs failed",
|
|
wantCalls: 2,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
path := "/tmp/root.ext4"
|
|
if tt.setup != nil {
|
|
path = tt.setup(t)
|
|
} else {
|
|
path = filepath.Join(t.TempDir(), "root.ext4")
|
|
if err := os.WriteFile(path, []byte("seed"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile(%s): %v", path, err)
|
|
}
|
|
}
|
|
var steps []systemStep
|
|
if tt.steps != nil {
|
|
steps = tt.steps(path)
|
|
}
|
|
runner := &scriptedRunner{t: t, steps: steps}
|
|
err := ResizeExt4Image(context.Background(), runner, path, 4096)
|
|
if err == nil {
|
|
t.Fatal("ResizeExt4Image() succeeded, want error")
|
|
}
|
|
if tt.wantErr != "" && !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Fatalf("ResizeExt4Image() error = %v, want %q", err, tt.wantErr)
|
|
}
|
|
if len(runner.calls) != tt.wantCalls {
|
|
t.Fatalf("call count = %d, want %d", len(runner.calls), tt.wantCalls)
|
|
}
|
|
runner.assertExhausted()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReadNormalizedLines(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
path := filepath.Join(t.TempDir(), "packages.apt")
|
|
if err := os.WriteFile(path, []byte("\n# comment\n git \nless # trailing\n\r\ntmux\r\n"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
|
|
got, err := ReadNormalizedLines(path)
|
|
if err != nil {
|
|
t.Fatalf("ReadNormalizedLines: %v", err)
|
|
}
|
|
want := []string{"git", "less", "tmux"}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("lines = %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestWriteExt4FileRemovesTempFileAndReturnsCopyError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
copyErr := errors.New("e2cp failed")
|
|
var tempPath string
|
|
runner := funcRunner{
|
|
runSudo: func(ctx context.Context, args ...string) ([]byte, error) {
|
|
switch args[0] {
|
|
case "e2rm":
|
|
return nil, errors.New("ignore remove")
|
|
case "e2cp":
|
|
tempPath = args[1]
|
|
if _, err := os.Stat(tempPath); err != nil {
|
|
t.Fatalf("temp file missing during e2cp: %v", err)
|
|
}
|
|
return nil, copyErr
|
|
default:
|
|
t.Fatalf("unexpected sudo call: %v", args)
|
|
return nil, nil
|
|
}
|
|
},
|
|
}
|
|
|
|
err := WriteExt4File(context.Background(), runner, "/tmp/root.ext4", "/etc/hostname", []byte("devbox\n"))
|
|
if !errors.Is(err, copyErr) {
|
|
t.Fatalf("WriteExt4File() error = %v, want %v", err, copyErr)
|
|
}
|
|
if tempPath == "" {
|
|
t.Fatal("expected e2cp temp path to be recorded")
|
|
}
|
|
if _, err := os.Stat(tempPath); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("temp file still exists after WriteExt4File: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMountTempDirUsesLoopForRegularFilesAndCleanupUsesBackgroundContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
source := filepath.Join(t.TempDir(), "root.ext4")
|
|
if err := os.WriteFile(source, []byte("rootfs"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
var mountDir string
|
|
calls := 0
|
|
runner := funcRunner{
|
|
runSudo: func(callCtx context.Context, args ...string) ([]byte, error) {
|
|
calls++
|
|
switch calls {
|
|
case 1:
|
|
mountDir = args[len(args)-1]
|
|
want := []string{"mount", "-o", "ro,loop", source, mountDir}
|
|
if !reflect.DeepEqual(args, want) {
|
|
t.Fatalf("mount args = %v, want %v", args, want)
|
|
}
|
|
case 2:
|
|
if callCtx.Err() != nil {
|
|
t.Fatalf("cleanup context should not be canceled: %v", callCtx.Err())
|
|
}
|
|
want := []string{"umount", mountDir}
|
|
if !reflect.DeepEqual(args, want) {
|
|
t.Fatalf("cleanup args = %v, want %v", args, want)
|
|
}
|
|
default:
|
|
t.Fatalf("unexpected RunSudo call %d: %v", calls, args)
|
|
}
|
|
return nil, nil
|
|
},
|
|
}
|
|
|
|
gotMountDir, cleanup, err := MountTempDir(ctx, runner, source, true)
|
|
if err != nil {
|
|
t.Fatalf("MountTempDir: %v", err)
|
|
}
|
|
if gotMountDir != mountDir {
|
|
t.Fatalf("mount dir = %q, want %q", gotMountDir, mountDir)
|
|
}
|
|
cancel()
|
|
if err := cleanup(); err != nil {
|
|
t.Fatalf("cleanup: %v", err)
|
|
}
|
|
if _, err := os.Stat(mountDir); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("mount dir still exists after cleanup: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseProcHelpers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
stat, err := parseProcStat("1234 (firecracker) S 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22")
|
|
if err != nil {
|
|
t.Fatalf("parseProcStat: %v", err)
|
|
}
|
|
if stat.userTicks != 11 || stat.systemTicks != 12 || stat.startTicks != 19 {
|
|
t.Fatalf("proc stat = %+v", stat)
|
|
}
|
|
|
|
statm, err := parseProcStatm("200 50 0 0 0 0 0")
|
|
if err != nil {
|
|
t.Fatalf("parseProcStatm: %v", err)
|
|
}
|
|
if statm.sizePages != 200 || statm.residentPages != 50 {
|
|
t.Fatalf("proc statm = %+v", statm)
|
|
}
|
|
|
|
uptime, err := parseProcUptime("321.50 42.10")
|
|
if err != nil {
|
|
t.Fatalf("parseProcUptime: %v", err)
|
|
}
|
|
if uptime != 321.50 {
|
|
t.Fatalf("uptime = %v, want 321.50", uptime)
|
|
}
|
|
}
|
|
|
|
func TestParseMemTotal(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
total, err := parseMemTotal("MemTotal: 131900020 kB\nMemFree: 1024 kB\n")
|
|
if err != nil {
|
|
t.Fatalf("parseMemTotal: %v", err)
|
|
}
|
|
if total != 131900020*1024 {
|
|
t.Fatalf("total = %d, want %d", total, int64(131900020*1024))
|
|
}
|
|
}
|
|
|
|
func TestParseMemTotalErrorsWhenMissing(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if _, err := parseMemTotal("MemFree: 123 kB\n"); err == nil {
|
|
t.Fatal("parseMemTotal() error = nil, want missing MemTotal")
|
|
}
|
|
}
|
|
|
|
func TestReadFilesystemUsage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
usage, err := ReadFilesystemUsage(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("ReadFilesystemUsage: %v", err)
|
|
}
|
|
if usage.TotalBytes <= 0 {
|
|
t.Fatalf("usage.TotalBytes = %d, want positive", usage.TotalBytes)
|
|
}
|
|
if usage.FreeBytes < 0 {
|
|
t.Fatalf("usage.FreeBytes = %d, want non-negative", usage.FreeBytes)
|
|
}
|
|
}
|
|
|
|
func TestMountTempDirRemovesTempDirWhenMountFails(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
source := t.TempDir()
|
|
var mountDir string
|
|
runner := funcRunner{
|
|
runSudo: func(ctx context.Context, args ...string) ([]byte, error) {
|
|
mountDir = args[len(args)-1]
|
|
return nil, errors.New("mount failed")
|
|
},
|
|
}
|
|
|
|
_, _, err := MountTempDir(context.Background(), runner, source, false)
|
|
if err == nil || !strings.Contains(err.Error(), "mount failed") {
|
|
t.Fatalf("MountTempDir() error = %v, want mount failure", err)
|
|
}
|
|
if mountDir == "" {
|
|
t.Fatal("expected mount path to be recorded")
|
|
}
|
|
if _, statErr := os.Stat(mountDir); !errors.Is(statErr, os.ErrNotExist) {
|
|
t.Fatalf("mount dir still exists after failed mount: %v", statErr)
|
|
}
|
|
}
|
|
|
|
func TestParseMetricsFileHandlesWholeAndLineDelimitedJSON(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
full := filepath.Join(t.TempDir(), "metrics-full.json")
|
|
if err := os.WriteFile(full, []byte(`{"uptime":1,"signals":{"sigterm":0}}`), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
got := ParseMetricsFile(full)
|
|
if got["uptime"] != float64(1) {
|
|
t.Fatalf("ParseMetricsFile(full) = %v", got)
|
|
}
|
|
|
|
lines := filepath.Join(t.TempDir(), "metrics-lines.json")
|
|
if err := os.WriteFile(lines, []byte("junk\n{\"uptime\":2}\n"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
got = ParseMetricsFile(lines)
|
|
if got["uptime"] != float64(2) {
|
|
t.Fatalf("ParseMetricsFile(lines) = %v", got)
|
|
}
|
|
}
|
|
|
|
func TestUpdateFSTabStripsLegacyMountsAndAddsDefaultsOnce(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
input := strings.Join([]string{
|
|
"/dev/vda / ext4 defaults 0 1",
|
|
"/dev/vdb /home ext4 defaults 0 2",
|
|
"/dev/vdc /var ext4 defaults 0 2",
|
|
"/dev/vdb /root ext4 defaults 0 2",
|
|
"tmpfs /run tmpfs defaults,nodev,nosuid,mode=0755 0 0",
|
|
"",
|
|
}, "\n")
|
|
|
|
got := UpdateFSTab(input)
|
|
if strings.Contains(got, "/home") || strings.Contains(got, "/var") {
|
|
t.Fatalf("UpdateFSTab() kept legacy mounts: %q", got)
|
|
}
|
|
if strings.Count(got, "/dev/vdb /root ext4 defaults 0 2") != 1 {
|
|
t.Fatalf("UpdateFSTab() duplicated /root mount: %q", got)
|
|
}
|
|
if strings.Count(got, "tmpfs /run tmpfs defaults,nodev,nosuid,mode=0755 0 0") != 1 {
|
|
t.Fatalf("UpdateFSTab() duplicated /run mount: %q", got)
|
|
}
|
|
if !strings.Contains(got, "tmpfs /tmp tmpfs defaults,nodev,nosuid,mode=1777 0 0") {
|
|
t.Fatalf("UpdateFSTab() missing /tmp mount: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestUseLoopMount(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
file := filepath.Join(t.TempDir(), "root.ext4")
|
|
if err := os.WriteFile(file, []byte("rootfs"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
dir := t.TempDir()
|
|
|
|
if !useLoopMount(file) {
|
|
t.Fatalf("useLoopMount(%s) = false, want true", file)
|
|
}
|
|
if useLoopMount(dir) {
|
|
t.Fatalf("useLoopMount(%s) = true, want false", dir)
|
|
}
|
|
if useLoopMount(filepath.Join(dir, "missing")) {
|
|
t.Fatalf("useLoopMount(missing) = true, want false")
|
|
}
|
|
}
|
|
|
|
func TestEstimateWorkSeedSizeFallsBackToSudoDuWhenUnreadable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rootHome := filepath.Join(t.TempDir(), "root")
|
|
if err := os.Mkdir(rootHome, 0o700); err != nil {
|
|
t.Fatalf("Mkdir: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(rootHome, "visible.txt"), []byte("seed"), 0o600); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
if err := os.Chmod(rootHome, 0o000); err != nil {
|
|
t.Fatalf("Chmod: %v", err)
|
|
}
|
|
defer os.Chmod(rootHome, 0o700)
|
|
|
|
var sudoCalled bool
|
|
runner := funcRunner{
|
|
runSudo: func(ctx context.Context, args ...string) ([]byte, error) {
|
|
sudoCalled = true
|
|
want := []string{"du", "-sb", rootHome}
|
|
if !reflect.DeepEqual(args, want) {
|
|
t.Fatalf("RunSudo args = %v, want %v", args, want)
|
|
}
|
|
return []byte("4096\t" + rootHome + "\n"), nil
|
|
},
|
|
}
|
|
|
|
sizeBytes, err := estimateWorkSeedSize(context.Background(), runner, rootHome)
|
|
if err != nil {
|
|
t.Fatalf("estimateWorkSeedSize: %v", err)
|
|
}
|
|
if !sudoCalled {
|
|
t.Fatal("estimateWorkSeedSize did not fall back to sudo du")
|
|
}
|
|
if sizeBytes != minWorkSeedBytes {
|
|
t.Fatalf("sizeBytes = %d, want %d", sizeBytes, minWorkSeedBytes)
|
|
}
|
|
}
|