server init now also: - runs a model mirror beside the control plane (/srv/models, bytes on :8081 at an address nodes reach, management API on loopback so it needs no certificate) and adds it to server.yaml; - makes the machine a node of its own control plane: issues its agent certificate, writes agent.yaml labelled as the control plane, starts llmgw-agent, and waits for it to register and install llama.cpp; - checks for NVIDIA GPUs with nvidia-smi, and reports a card on the PCI bus whose driver does not answer rather than ignoring it; - fetches Qwen2.5 0.5B (Q4_K_M) into the mirror and adds it to the catalog pinned to this node: a llama.cpp profile per GPU class found, with a CPU profile as the fallback, then waits for it to serve. --no-mirror, --no-node and --no-model leave these out. Every step looks before it acts, so a rerun finishes what an interrupted one started. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
90 lines
2.6 KiB
Go
90 lines
2.6 KiB
Go
package cli
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/config"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog"
|
|
)
|
|
|
|
func TestTheInitAgentUnitIsTheShippedUnit(t *testing.T) {
|
|
b, err := os.ReadFile("../../deploy/systemd/llmgw-agent.service")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(b) != agentUnit {
|
|
t.Error("deploy/systemd/llmgw-agent.service and the unit 'server init' writes have drifted")
|
|
}
|
|
}
|
|
|
|
func TestTheStarterUsesTheGPUAndFallsBackToCPUOnThisMachine(t *testing.T) {
|
|
m := starterModel("http://10.0.0.5:8081/qwen2.5-0.5b-instruct/starter/q.gguf", []string{"4090-24g"})
|
|
c, err := catalog.New([]catalog.Model{m})
|
|
if err != nil {
|
|
t.Fatalf("the starter is not a valid catalog entry: %v", err)
|
|
}
|
|
got, _ := c.Resolve(starterName)
|
|
if len(got.Profiles) != 2 {
|
|
t.Fatalf("profiles = %+v", got.Profiles)
|
|
}
|
|
gpu, cpu := got.Profiles[0], got.Profiles[1]
|
|
if gpu.GPUClass != "4090-24g" || gpu.Priority <= cpu.Priority {
|
|
t.Errorf("the GPU profile should come first: %+v", gpu)
|
|
}
|
|
if cpu.GPUClass != "" || cpu.Engine != "llamacpp" {
|
|
t.Errorf("the fallback should be llama.cpp on CPU: %+v", cpu)
|
|
}
|
|
for _, p := range got.Profiles {
|
|
if p.NodeSelector[localLabelKey] != localLabelValue {
|
|
t.Errorf("profile %s is not pinned to this machine", p.Name)
|
|
}
|
|
}
|
|
if got.Scaling.Min != 1 {
|
|
t.Error("the starter should be running, not waiting to be asked for")
|
|
}
|
|
|
|
if m := starterModel("http://x/q.gguf", nil); len(m.Profiles) != 1 || m.Profiles[0].Name != "cpu" {
|
|
t.Errorf("without a GPU only the CPU profile: %+v", m.Profiles)
|
|
}
|
|
}
|
|
|
|
func TestTheMirrorBlockIsAddedAndReads(t *testing.T) {
|
|
in, out := newTestInit(t)
|
|
if err := in.run(t.Context()); err != nil {
|
|
t.Fatalf("%v\n%s", err, out)
|
|
}
|
|
if err := appendMirrorBlock(in.configPath()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := config.Load(in.configPath())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.Mirror.API != "http://"+mirrorAPI {
|
|
t.Errorf("mirror.api = %q", cfg.Mirror.API)
|
|
}
|
|
if err := cfg.Validate(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
backups, _ := filepath.Glob(in.configPath() + ".bak-*")
|
|
if len(backups) != 1 {
|
|
t.Errorf("backups = %v", backups)
|
|
}
|
|
}
|
|
|
|
func TestTheMirrorUnitAdvertisesAnAddressNodesCanReach(t *testing.T) {
|
|
in := &serverInit{hosts: []string{"10.0.0.140"}}
|
|
u := in.mirrorUnit()
|
|
if !strings.Contains(u, "--base http://10.0.0.140:8081") {
|
|
t.Errorf("base: %s", u)
|
|
}
|
|
// The management port can delete models; nothing but this machine may
|
|
// reach it, which is what lets it go without a certificate.
|
|
if !strings.Contains(u, "--api-listen 127.0.0.1:8082") {
|
|
t.Errorf("the management API is not on loopback: %s", u)
|
|
}
|
|
}
|