Files
llmgw/internal/onboard/onboard_test.go
T
Ilker ManapandClaude Opus 5.5 ad04a78343 The mirror's address is one the nodes reach, and node add checks it
server init and mirror init took the first non-loopback address for the
mirror's --base, Docker's 172.17.0.1 on a machine with Docker; nodes asked
their own bridge for every model and were refused. primaryAddr takes the
default route's source, else the first address on an interface that is not
a container's, bridge's or overlay's. node add has the node reach the
mirror's files during the checks and fails, naming the address and the fix,
when it cannot.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 09:36:09 +02:00

423 lines
13 KiB
Go

package onboard
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"os/exec"
"regexp"
"runtime"
"strings"
"sync"
"testing"
"gitea.mynodes.xyz/ilker/llmgw/internal/agent/discovery"
"gitea.mynodes.xyz/ilker/llmgw/internal/doctor"
"gitea.mynodes.xyz/ilker/llmgw/internal/release"
"gitea.mynodes.xyz/ilker/llmgw/internal/tlsconf"
)
// fakeMachine answers what onboarding asks a machine, and keeps what it is
// given.
type fakeMachine struct {
mu sync.Mutex
hostname string
arch string
noSudo bool
report doctorReport
files map[string][]byte
scripts []string
download error
// mirror is what curl prints for the mirror's address: 000 unreachable.
mirror string
}
type doctorReport struct {
Findings []doctor.Finding `json:"findings"`
Facts doctor.Facts `json:"facts"`
}
var putPath = regexp.MustCompile(`cat > (\S+)\.new`)
func (f *fakeMachine) Run(_ context.Context, script string, stdin []byte) ([]byte, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.scripts = append(f.scripts, script)
switch {
case script == "hostname && uname -m":
return []byte(f.hostname + "\n" + f.arch + "\n"), nil
case script == "sudo -n true":
if f.noSudo {
return []byte("sudo: a password is required"), errors.New("exit 1")
}
return nil, nil
case strings.Contains(script, "install -m 0755") && stdin != nil:
f.files["/usr/local/bin/llmgw"] = stdin
return nil, nil
case strings.HasPrefix(script, "sudo llmgw doctor --json"):
b, _ := json.Marshal(f.report)
return b, nil
case putPath.MatchString(script):
f.files[putPath.FindStringSubmatch(script)[1]] = stdin
return nil, nil
case strings.Contains(script, "systemctl restart llmgw-agent"):
return []byte("active\n"), nil
case strings.Contains(script, "curl -s -o /dev/null -w"):
return []byte(f.mirror), nil
}
return nil, nil
}
func (f *fakeMachine) ran(s string) bool {
for _, x := range f.scripts {
if strings.Contains(x, s) {
return true
}
}
return false
}
func newFake() *fakeMachine {
return &fakeMachine{hostname: "gpu01", arch: map[string]string{"amd64": "x86_64", "arm64": "aarch64"}[runtime.GOARCH],
files: map[string][]byte{}}
}
func runner(t *testing.T, m *fakeMachine, o Options) (*Runner, *[]Event) {
t.Helper()
pki := t.TempDir()
ca, err := tlsconf.NewCA("test")
if err != nil {
t.Fatal(err)
}
if err := ca.Write(pki); err != nil {
t.Fatal(err)
}
o.Addr = "10.0.0.21"
o.PKI, o.EtcDir = pki, t.TempDir()
if o.Servers == nil {
o.Servers = []string{"https://10.0.0.10:7443"}
}
o.Binary = "onboard_test.go" // anything readable
AgentUnit = "[Unit]\n"
var events []Event
var mu sync.Mutex
return &Runner{Opts: o, Remote: m, Emit: func(e Event) { mu.Lock(); events = append(events, e); mu.Unlock() }}, &events
}
func TestANodeIsAddedFromItsAddressAlone(t *testing.T) {
m := newFake()
r, _ := runner(t, m, Options{Version: "v1.2.3-4-gabc"})
res, err := r.Run(t.Context())
if err != nil {
t.Fatal(err)
}
if res.Node != "gpu01" {
t.Errorf("node id %q, want the machine's hostname", res.Node)
}
for _, f := range []string{"/etc/llmgw/ca.pem", "/etc/llmgw/agent.pem", "/etc/llmgw/agent-key.pem",
"/etc/llmgw/agent.yaml", "/etc/systemd/system/llmgw-agent.service", "/usr/local/bin/llmgw"} {
if len(m.files[f]) == 0 {
t.Errorf("%s was not written", f)
}
}
yaml := string(m.files["/etc/llmgw/agent.yaml"])
if !strings.Contains(yaml, `server: "https://10.0.0.10:7443"`) || strings.Contains(yaml, "node_id") {
t.Errorf("agent.yaml:\n%s", yaml)
}
// The key is never readable by others, not even for a moment on the way.
if !m.ran("umask 077; cat > /etc/llmgw/agent-key.pem.new") || !m.ran("chmod 0600 /etc/llmgw/agent-key.pem.new") {
t.Error("the key was not written private")
}
if !m.ran("systemctl restart llmgw-agent") {
t.Error("the agent was not started")
}
}
// releaseFor serves one release for linux/<arch>, its checksums signed with
// a key the test then trusts; tamper changes the binary after signing.
func releaseFor(t *testing.T, version, arch string, body []byte, signed, tamper bool) string {
t.Helper()
pub, priv, err := release.GenerateKey()
if err != nil {
t.Fatal(err)
}
t.Setenv("LLMGW_RELEASE_KEY", pub)
asset := release.AssetName(version, "linux", arch)
sum := sha256.Sum256(body)
sums := hex.EncodeToString(sum[:]) + " " + asset + "\n"
sig, _ := release.Sign(priv, []byte(sums))
if tamper {
body = []byte("tampered")
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/" + version + "/checksums.txt":
_, _ = w.Write([]byte(sums))
case "/" + version + "/" + release.SignatureName:
if !signed {
http.NotFound(w, r)
return
}
_, _ = w.Write([]byte(sig))
case "/" + version + "/" + asset:
_, _ = w.Write(body)
default:
http.NotFound(w, r)
}
}))
t.Cleanup(srv.Close)
return srv.URL
}
func TestAReleaseIsFetchedAndCheckedHereThenGivenToTheNode(t *testing.T) {
m := newFake()
base := releaseFor(t, "v1.2.3", hostArch(), []byte("the release"), true, false)
r, events := runner(t, m, Options{Version: "v1.2.3", Releases: base})
if _, err := r.Run(t.Context()); err != nil {
t.Fatal(err)
}
if got := string(m.files["/usr/local/bin/llmgw"]); got != "the release" {
t.Errorf("the node was given %q", got)
}
if m.ran("curl") {
t.Error("the node fetched something itself")
}
if !said(*events, Done, "signature and checksum verified") {
t.Errorf("the check was not reported: %v", *events)
}
}
func TestAnUnsignedOrAlteredReleaseIsNotGivenToTheNode(t *testing.T) {
for name, c := range map[string][2]bool{"unsigned": {false, false}, "altered": {true, true}} {
m := newFake()
base := releaseFor(t, "v1.2.3", hostArch(), []byte("the release"), c[0], c[1])
r, events := runner(t, m, Options{Version: "v1.2.3", Releases: base})
_, _ = r.Run(t.Context())
if got := string(m.files["/usr/local/bin/llmgw"]); got == "the release" || got == "tampered" {
t.Errorf("%s: the node was given the release", name)
}
if !said(*events, Warn, "could not be fetched and checked") {
t.Errorf("%s: the refusal was not said", name)
}
}
}
func TestANodeGetsThisMachinesCopyWhenTheReleaseCannotBeReached(t *testing.T) {
m := newFake()
r, events := runner(t, m, Options{Version: "v1.2.3", Releases: "https://127.0.0.1:1"})
if _, err := r.Run(t.Context()); err != nil {
t.Fatal(err)
}
if len(m.files["/usr/local/bin/llmgw"]) == 0 {
t.Error("the binary was not copied")
}
if !said(*events, Warn, "could not be fetched") {
t.Error("the failed download was not mentioned")
}
}
func TestAnotherArchitectureIsNotGivenThisMachinesBinary(t *testing.T) {
m := newFake()
m.arch = map[string]string{"amd64": "aarch64", "arm64": "x86_64"}[runtime.GOARCH]
r, _ := runner(t, m, Options{Version: "dev"})
_, err := r.Run(t.Context())
if err == nil || !strings.Contains(err.Error(), "cannot be copied") {
t.Errorf("got %v", err)
}
}
func TestNoSudoIsSaidPlainly(t *testing.T) {
m := newFake()
m.noSudo = true
r, _ := runner(t, m, Options{})
_, err := r.Run(t.Context())
if err == nil || !strings.Contains(err.Error(), "sudo without a password") {
t.Errorf("got %v", err)
}
if len(m.files) > 0 {
t.Error("the machine was changed before finding it could not be finished")
}
}
func TestADoctorProblemStopsItUnlessAnswered(t *testing.T) {
fail := doctor.Finding{Level: doctor.Fail, Area: "nvlink", Message: "fabric manager inactive", Fix: "systemctl enable --now nvidia-fabricmanager"}
m := newFake()
m.report.Findings = []doctor.Finding{fail}
r, events := runner(t, m, Options{Version: "dev"})
if _, err := r.Run(t.Context()); !errors.Is(err, ErrChecks) {
t.Fatalf("got %v, want ErrChecks", err)
}
if !said(*events, Fail, "fabric manager inactive") {
t.Error("the problem itself was not reported")
}
if _, ok := m.files["/etc/llmgw/agent.pem"]; ok {
t.Error("a certificate was installed on a machine that failed its checks")
}
m = newFake()
m.report.Findings = []doctor.Finding{fail}
asked := 0
r, _ = runner(t, m, Options{Version: "dev", AskContinue: func(n int) bool { asked = n; return true }})
if _, err := r.Run(t.Context()); err != nil {
t.Fatal(err)
}
if asked != 1 {
t.Errorf("asked about %d problems, want 1", asked)
}
}
func ibPort(sm string) discovery.Port {
return discovery.Port{Num: 1, State: "ACTIVE", LinkLayer: "InfiniBand", SM: &discovery.SMInfo{GUID: sm}}
}
func TestAdaptersAreGroupedByTheFabricTheyReport(t *testing.T) {
hcas := []discovery.HCA{
{Name: "mlx5_0", Ports: []discovery.Port{ibPort("0xa")}},
{Name: "mlx5_1", Ports: []discovery.Port{ibPort("0xa")}},
{Name: "mlx5_4", Ports: []discovery.Port{ibPort("0xb")}},
{Name: "mlx5_5", Ports: []discovery.Port{{Num: 1, State: "DOWN"}}},
}
got := suggestFabrics(hcas)
if len(got) != 2 || strings.Join(got[0].HCAs, ",") != "mlx5_0,mlx5_1" || strings.Join(got[1].HCAs, ",") != "mlx5_4" {
t.Errorf("got %+v", got)
}
}
func TestTheFabricsChosenAreWrittenToTheAgent(t *testing.T) {
m := newFake()
m.report.Facts.HCAs = []discovery.HCA{{Name: "mlx5_0", Ports: []discovery.Port{ibPort("0xa")}}}
r, _ := runner(t, m, Options{Version: "dev", AskFabrics: func(s []Fabric) []Fabric {
return []Fabric{{Name: "ib-a", HCAs: s[0].HCAs}}
}})
if _, err := r.Run(t.Context()); err != nil {
t.Fatal(err)
}
if yaml := string(m.files["/etc/llmgw/agent.yaml"]); !strings.Contains(yaml, `- name: "ib-a"`) || !strings.Contains(yaml, "hcas: [mlx5_0]") {
t.Errorf("agent.yaml:\n%s", yaml)
}
// Asked nothing, a script is told what to add instead.
m = newFake()
m.report.Facts.HCAs = []discovery.HCA{{Name: "mlx5_0", Ports: []discovery.Port{ibPort("0xa")}}}
r, events := runner(t, m, Options{Version: "dev"})
if _, err := r.Run(t.Context()); err != nil {
t.Fatal(err)
}
if !said(*events, Warn, "no fabric was given") {
t.Error("adapters up with no fabric given went unmentioned")
}
}
func TestANodeNamedOtherThanItsHostnameSaysSo(t *testing.T) {
res := &Result{Node: "gpu-a", Hostname: "ip-10-0-0-21", Servers: []string{"https://a:7443", "https://b:7443"}}
y := AgentYAML(Options{}, res)
if !strings.Contains(y, `node_id: "gpu-a"`) || !strings.Contains(y, "servers:\n - \"https://a:7443\"") {
t.Errorf("agent.yaml:\n%s", y)
}
}
func TestParseFabric(t *testing.T) {
f, err := ParseFabric("ib-a:mlx5_0, mlx5_1")
if err != nil || f.Name != "ib-a" || strings.Join(f.HCAs, ",") != "mlx5_0,mlx5_1" {
t.Errorf("got %+v %v", f, err)
}
if _, err := ParseFabric(":mlx5_0"); err == nil {
t.Error("a fabric with no name was accepted")
}
}
func said(events []Event, level, s string) bool {
for _, e := range events {
if e.Level == level && strings.Contains(e.Detail, s) {
return true
}
}
return false
}
func TestAnEngineDisabledOnANodeIsWrittenAndNotChecked(t *testing.T) {
m := newFake()
r, events := runner(t, m, Options{Version: "dev", DisableEngines: []string{"vllm"}, VLLMVersion: "0.30.0"})
if _, err := r.Run(t.Context()); err != nil {
t.Fatal(err)
}
if yaml := string(m.files["/etc/llmgw/agent.yaml"]); !strings.Contains(yaml, "engines:\n disabled: [vllm]") {
t.Errorf("agent.yaml:\n%s", yaml)
}
if !m.ran("sudo llmgw doctor --json --vllm none") {
t.Errorf("doctor was asked about a vLLM this node will never run: %v", m.scripts)
}
if !said(*events, Info, "joins with llama.cpp alone") {
t.Error("the reduced engine set went unsaid")
}
// Not disabled: doctor checks the vLLM the control plane installs, not
// its own default.
m = newFake()
r, _ = runner(t, m, Options{Version: "dev", VLLMVersion: "0.25.1"})
if _, err := r.Run(t.Context()); err != nil {
t.Fatal(err)
}
if !m.ran("sudo llmgw doctor --json --vllm '0.25.1'") && !m.ran("sudo llmgw doctor --json --vllm 0.25.1") {
t.Errorf("doctor was not told the control plane's vLLM: %v", m.scripts)
}
}
func TestTheFabricsGivenAreCheckedAsDeclared(t *testing.T) {
// agent.yaml is written after doctor runs, so the fabrics on the command
// line are handed to doctor: checked strictly, and nothing else is.
m := newFake()
r, _ := runner(t, m, Options{Version: "dev", Fabrics: []Fabric{{Name: "ib-a", HCAs: []string{"mlx5_0", "mlx5_1"}}}})
if _, err := r.Run(t.Context()); err != nil {
t.Fatal(err)
}
if !m.ran("--fabric 'ib-a:mlx5_0,mlx5_1'") {
t.Errorf("doctor was not told the fabric: %v", m.scripts)
}
}
func TestTheSudoGrantRemovesItselfWithinTheHour(t *testing.T) {
inner := sudoGrantScript("ilker")
if !strings.Contains(inner, "--on-active=3600") || !strings.Contains(inner, "sleep 3600; rm -f "+sudoersDrop) {
t.Errorf("no removal scheduled: %s", inner)
}
if !strings.Contains(inner, "visudo -cf") {
t.Error("written without visudo's check")
}
if out, err := exec.Command("sh", "-n", "-c", inner).CombinedOutput(); err != nil {
t.Errorf("not shell: %v %s\n%s", err, out, inner)
}
}
func TestANodeThatCannotReachTheMirrorIsNotAdded(t *testing.T) {
m := newFake()
m.mirror = "000"
r, events := runner(t, m, Options{Addr: "10.0.0.9", MirrorBase: "http://172.17.0.1:8081"})
var res Result
err := r.check(t.Context(), &res)
if !errors.Is(err, ErrChecks) {
t.Fatalf("a node that cannot reach the mirror was let through: %v", err)
}
found := false
for _, e := range *events {
if e.Level == Fail && strings.Contains(e.Detail, "http://172.17.0.1:8081/") && strings.Contains(e.Fix, "--base") {
found = true
}
}
if !found {
t.Errorf("no failure naming the address and the fix: %+v", *events)
}
ok := newFake()
ok.mirror = "404"
r, _ = runner(t, ok, Options{Addr: "10.0.0.9", MirrorBase: "http://10.0.0.146:8081"})
if err := r.check(t.Context(), &res); err != nil {
t.Errorf("a node that reaches the mirror: %v", err)
}
}