Post-repair hardware validation pipeline for Proxmox cluster hosts. Go orchestrator + in-image agent + mkosi live image + bundled dnsmasq PXE + SQLite + HTMX/SSE UI + notify registry + janitor + full docs.
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
package store_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"vetting/internal/db"
|
||||
"vetting/internal/model"
|
||||
"vetting/internal/store"
|
||||
)
|
||||
|
||||
func newDB(t *testing.T) *store.Runs {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "vetting.db")
|
||||
conn, err := db.Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = conn.Close() })
|
||||
return &store.Runs{DB: conn}
|
||||
}
|
||||
|
||||
// seedRun inserts a host + a run and returns (hostID, runID). Every
|
||||
// subsequent store test builds on this so run_id foreign keys resolve.
|
||||
func seedRun(t *testing.T, runs *store.Runs) (int64, int64) {
|
||||
t.Helper()
|
||||
hosts := &store.Hosts{DB: runs.DB}
|
||||
hostID, err := hosts.Create(context.Background(), model.Host{
|
||||
Name: "t-host",
|
||||
MAC: "aa:bb:cc:dd:ee:ff",
|
||||
WoLBroadcastIP: "10.0.0.255",
|
||||
WoLPort: 9,
|
||||
ExpectedSpecYAML: "memory:\n total_gib: 16\n",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create host: %v", err)
|
||||
}
|
||||
runID, err := runs.Create(context.Background(), hostID, "deadbeef")
|
||||
if err != nil {
|
||||
t.Fatalf("create run: %v", err)
|
||||
}
|
||||
return hostID, runID
|
||||
}
|
||||
|
||||
func TestArtifactsRoundtrip(t *testing.T) {
|
||||
runs := newDB(t)
|
||||
_, runID := seedRun(t, runs)
|
||||
arts := &store.Artifacts{DB: runs.DB}
|
||||
|
||||
id, err := arts.Create(context.Background(), store.Artifact{
|
||||
RunID: runID,
|
||||
Kind: "inventory",
|
||||
Path: "/var/artifacts/run-1/inventory.json",
|
||||
SHA256: "abc123",
|
||||
SizeBytes: 42,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
if id == 0 {
|
||||
t.Fatalf("expected non-zero id")
|
||||
}
|
||||
|
||||
// Hold key on the same run — ListForRun should return both in
|
||||
// insertion order and TileEnricher picks the hold_key row.
|
||||
if _, err := arts.Create(context.Background(), store.Artifact{
|
||||
RunID: runID, Kind: "hold_key", Path: "/var/artifacts/run-1/hold.key", SHA256: "def456", SizeBytes: 400,
|
||||
}); err != nil {
|
||||
t.Fatalf("Create hold_key: %v", err)
|
||||
}
|
||||
|
||||
list, err := arts.ListForRun(context.Background(), runID)
|
||||
if err != nil {
|
||||
t.Fatalf("ListForRun: %v", err)
|
||||
}
|
||||
if len(list) != 2 {
|
||||
t.Fatalf("ListForRun returned %d, want 2", len(list))
|
||||
}
|
||||
if list[0].Kind != "inventory" || list[1].Kind != "hold_key" {
|
||||
t.Fatalf("unexpected order: %+v", list)
|
||||
}
|
||||
if list[1].Path != "/var/artifacts/run-1/hold.key" {
|
||||
t.Fatalf("hold_key path lost: %q", list[1].Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecDiffsReplaceForRun(t *testing.T) {
|
||||
runs := newDB(t)
|
||||
_, runID := seedRun(t, runs)
|
||||
sd := &store.SpecDiffs{DB: runs.DB}
|
||||
ctx := context.Background()
|
||||
|
||||
// First write: three diffs.
|
||||
err := sd.ReplaceForRun(ctx, runID, []model.SpecDiff{
|
||||
{RunID: runID, Field: "cpu.model", Expected: "Xeon", Actual: "EPYC", Severity: "critical"},
|
||||
{RunID: runID, Field: "memory.total_gib", Expected: "16", Actual: "8", Severity: "critical"},
|
||||
{RunID: runID, Field: "note", Expected: "", Actual: "dusty", Severity: "info"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ReplaceForRun: %v", err)
|
||||
}
|
||||
|
||||
list, err := sd.ListForRun(ctx, runID)
|
||||
if err != nil {
|
||||
t.Fatalf("ListForRun: %v", err)
|
||||
}
|
||||
if len(list) != 3 {
|
||||
t.Fatalf("got %d rows, want 3", len(list))
|
||||
}
|
||||
|
||||
// Second write replaces, doesn't append — otherwise a re-run would
|
||||
// double-count spec diffs and the tile badge would grow without bound.
|
||||
err = sd.ReplaceForRun(ctx, runID, []model.SpecDiff{
|
||||
{RunID: runID, Field: "cpu.model", Expected: "Xeon", Actual: "Xeon Gold", Severity: "info"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("second ReplaceForRun: %v", err)
|
||||
}
|
||||
list, err = sd.ListForRun(ctx, runID)
|
||||
if err != nil {
|
||||
t.Fatalf("ListForRun after replace: %v", err)
|
||||
}
|
||||
if len(list) != 1 {
|
||||
t.Fatalf("expected 1 row after replace, got %d", len(list))
|
||||
}
|
||||
if list[0].Severity != "info" {
|
||||
t.Fatalf("expected severity info, got %q", list[0].Severity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeasurementsBatchAndList(t *testing.T) {
|
||||
runs := newDB(t)
|
||||
_, runID := seedRun(t, runs)
|
||||
meas := &store.Measurements{DB: runs.DB}
|
||||
ctx := context.Background()
|
||||
|
||||
err := meas.CreateBatch(ctx, []model.Measurement{
|
||||
{RunID: runID, Kind: "thermal", Key: "cpu", Value: 52.5, Unit: "C"},
|
||||
{RunID: runID, Kind: "iperf", Key: "throughput_mbps", Value: 940.1, Unit: "Mbps"},
|
||||
{RunID: runID, Kind: "psu", Key: "in0", Value: 12.04, Unit: "V"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateBatch: %v", err)
|
||||
}
|
||||
|
||||
// Zero-length batch must be a no-op, not an error.
|
||||
if err := meas.CreateBatch(ctx, nil); err != nil {
|
||||
t.Fatalf("empty CreateBatch: %v", err)
|
||||
}
|
||||
|
||||
rows, err := meas.ListForRun(ctx, runID)
|
||||
if err != nil {
|
||||
t.Fatalf("ListForRun: %v", err)
|
||||
}
|
||||
if len(rows) != 3 {
|
||||
t.Fatalf("got %d rows, want 3", len(rows))
|
||||
}
|
||||
foundIperf := false
|
||||
for _, r := range rows {
|
||||
if r.Kind == "iperf" && r.Key == "throughput_mbps" && r.Value > 900 {
|
||||
foundIperf = true
|
||||
}
|
||||
}
|
||||
if !foundIperf {
|
||||
t.Fatalf("iperf row missing or wrong value: %+v", rows)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunsOverrideFlagsAndClearFailedStage(t *testing.T) {
|
||||
runs := newDB(t)
|
||||
_, runID := seedRun(t, runs)
|
||||
ctx := context.Background()
|
||||
|
||||
if err := runs.SetFailedStage(ctx, runID, "Storage"); err != nil {
|
||||
t.Fatalf("SetFailedStage: %v", err)
|
||||
}
|
||||
if err := runs.SetOverrideFlags(ctx, runID, `{"wipe":true}`); err != nil {
|
||||
t.Fatalf("SetOverrideFlags: %v", err)
|
||||
}
|
||||
run, err := runs.Get(ctx, runID)
|
||||
if err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
if run.OverrideFlagsJSON != `{"wipe":true}` {
|
||||
t.Fatalf("OverrideFlagsJSON = %q, want {\"wipe\":true}", run.OverrideFlagsJSON)
|
||||
}
|
||||
if run.FailedStage != "Storage" {
|
||||
t.Fatalf("FailedStage = %q, want Storage", run.FailedStage)
|
||||
}
|
||||
if err := runs.ClearFailedStage(ctx, runID); err != nil {
|
||||
t.Fatalf("ClearFailedStage: %v", err)
|
||||
}
|
||||
run, err = runs.Get(ctx, runID)
|
||||
if err != nil {
|
||||
t.Fatalf("Get after clear: %v", err)
|
||||
}
|
||||
if run.FailedStage != "" {
|
||||
t.Fatalf("FailedStage not cleared: %q", run.FailedStage)
|
||||
}
|
||||
// override_flags_json should persist across ClearFailedStage so the
|
||||
// agent can still read it on its next heartbeat.
|
||||
if run.OverrideFlagsJSON != `{"wipe":true}` {
|
||||
t.Fatalf("OverrideFlagsJSON lost after ClearFailedStage: %q", run.OverrideFlagsJSON)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunsHoldAndFailedStage(t *testing.T) {
|
||||
runs := newDB(t)
|
||||
_, runID := seedRun(t, runs)
|
||||
ctx := context.Background()
|
||||
|
||||
if err := runs.SetHoldIP(ctx, runID, "10.0.0.42"); err != nil {
|
||||
t.Fatalf("SetHoldIP: %v", err)
|
||||
}
|
||||
if err := runs.SetFailedStage(ctx, runID, "SpecValidate"); err != nil {
|
||||
t.Fatalf("SetFailedStage: %v", err)
|
||||
}
|
||||
run, err := runs.Get(ctx, runID)
|
||||
if err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
if run.HoldIP != "10.0.0.42" {
|
||||
t.Fatalf("HoldIP = %q, want 10.0.0.42", run.HoldIP)
|
||||
}
|
||||
if run.FailedStage != "SpecValidate" {
|
||||
t.Fatalf("FailedStage = %q, want SpecValidate", run.FailedStage)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user