mirror init sets a machine up as the model mirror — the user, /srv/models, and a unit serving files on :8081 and the management API on :8082 — and server init --mirror points the control plane at it instead of running one. Before, that took a unit written by hand and an edit to server.yaml. The management API can fetch and delete models and answered anyone who could reach it. mirror serve --api-allow limits it to the addresses given and the machine itself; mirror init passes the control plane's. The unit text is shared with the one server init writes for a mirror beside it. With --no-node the starter model is still fetched and catalogued, unpinned, so it starts on the first node added, and the init form shows the mirror in use instead of offering to run one. Tried on three empty VMs: the mirror answered the control plane and refused the node, and the starter model came up on the node when it was added. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
124 lines
3.7 KiB
Go
124 lines
3.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"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"}, true)
|
|
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, true); 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 := appendMirrorBlockFor(in.configPath(), "http://"+mirrorAPI, "# the mirror"); 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)
|
|
}
|
|
}
|
|
|
|
func TestWithNoNodeHereTheStarterGoesToTheFirstNodeThatJoins(t *testing.T) {
|
|
m := starterModel("http://x/q.gguf", nil, false)
|
|
for _, p := range m.Profiles {
|
|
if len(p.NodeSelector) > 0 {
|
|
t.Errorf("profile %s is pinned to a machine that is not a node: %v", p.Name, p.NodeSelector)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheMirrorAPIOnlyAnswersItsControlPlane(t *testing.T) {
|
|
allowed, err := parseAllow([]string{"10.0.0.140", "10.1.0.0/24"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h := onlyFrom(allowed, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {}))
|
|
for addr, want := range map[string]int{
|
|
"10.0.0.140:5000": 200, "10.1.0.7:5000": 200, "127.0.0.1:5000": 200, "[::1]:5000": 200,
|
|
"10.0.0.141:5000": 403, "192.168.1.9:5000": 403,
|
|
} {
|
|
r := httptest.NewRequest("GET", "/api/v1/health", nil)
|
|
r.RemoteAddr = addr
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != want {
|
|
t.Errorf("%s: %d, want %d", addr, w.Code, want)
|
|
}
|
|
}
|
|
if _, err := parseAllow([]string{"not-an-address"}); err == nil {
|
|
t.Error("nonsense was accepted")
|
|
}
|
|
}
|