diff --git a/CHANGELOG.md b/CHANGELOG.md index c33c4ca..86cfc55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,14 @@ is only released once its **exit criterion** is met, not when its code lands. Until v1.0.0 the minor version is the breaking one: configuration formats and Go APIs change between minors without a deprecation period. +## Unreleased + +### Fixed + +- **`llmgw mirror add-peer` on a mirror with a machine of its own.** It set the second mirror up and then + failed for want of the control plane's server.yaml; now it prints the `llmgw ctl config set mirror.peers` + to run on a control plane. + ## v0.15.2 — 2026-10-29 A served model moved to more cards, or fewer, while it is served. diff --git a/docs/install.md b/docs/install.md index cdd064d..d107c4f 100644 --- a/docs/install.md +++ b/docs/install.md @@ -2583,6 +2583,12 @@ is down, and new ones wait for it. With a second, the cluster carries on. ``` sudo llmgw mirror add-peer ubuntu@10.0.0.7 --control-plane 10.0.0.5 --control-plane 10.0.0.6 --control-plane 10.0.0.7 ``` + + It logs in with root's ssh key on the mirror's machine, so that key must + be accepted for the user named on the second machine. When the mirror has + a machine of its own, without the control plane, the command cannot change + the cluster's settings itself: it prints the `llmgw ctl config set + mirror.peers …` to run on a control plane. - **Kept in step:** each mirror reads the other every thirty seconds and copies what it lacks, keeping a copy only if its checksum matches. - A model removed from one is removed from the other, even if that one diff --git a/internal/cli/mirror_peer.go b/internal/cli/mirror_peer.go index 0a51373..13a2a92 100644 --- a/internal/cli/mirror_peer.go +++ b/internal/cli/mirror_peer.go @@ -34,6 +34,8 @@ step with this one: each copies what the other holds, checksum checked, and a model removed from one is removed from the other. This mirror is told of it, and the cluster's settings name it (mirror.peers), so while either machine is down the control planes use the other and nodes fetch from it. +On a mirror machine without the control plane's settings file, it prints +the llmgw ctl config set to run on a control plane instead. HOST needs room for every model this mirror holds: it copies them all.`, Example: " sudo llmgw mirror add-peer ubuntu@10.0.0.12 --control-plane 10.0.0.10 --control-plane 10.0.0.12", @@ -123,6 +125,19 @@ func addMirrorPeer(ctx context.Context, out io.Writer, remote onboard.Remote, ho fmt.Fprintf(out, " ok this mirror keeps in step with %s\n", peerAPI) // The control planes told of both. + return nameMirrorPeer(out, configPath, peerAPI) +} + +// nameMirrorPeer names a peer in the cluster's settings. A mirror on a +// machine of its own has neither the settings file nor the store: it says +// what to run on a control plane. +func nameMirrorPeer(out io.Writer, configPath, peerAPI string) error { + if _, err := os.Stat(configPath); errors.Is(err, os.ErrNotExist) { + fmt.Fprintf(out, " !! this machine has no %s, so the cluster's settings are not changed here. On a control plane:\n"+ + " sudo llmgw ctl config set mirror.peers '[%s]'\n"+ + " (with any peers already named, which llmgw ctl config get mirror.peers shows)\n", configPath, peerAPI) + return nil + } if err := addMirrorPeerSetting(configPath, peerAPI); err != nil { return err } diff --git a/internal/cli/mirror_peer_test.go b/internal/cli/mirror_peer_test.go index 6556ef6..7b4b745 100644 --- a/internal/cli/mirror_peer_test.go +++ b/internal/cli/mirror_peer_test.go @@ -1,6 +1,7 @@ package cli import ( + "bytes" "os" "path/filepath" "strings" @@ -72,3 +73,13 @@ func TestTheSetupOffersASecondMirrorOnAStoreMachine(t *testing.T) { t.Errorf("without standbys: %v", err) } } + +func TestAMirrorOnAMachineOfItsOwnSaysWhatToRunOnAControlPlane(t *testing.T) { + var out bytes.Buffer + if err := nameMirrorPeer(&out, filepath.Join(t.TempDir(), "server.yaml"), "http://10.0.0.12:8082"); err != nil { + t.Fatal(err) + } + if !strings.Contains(out.String(), "llmgw ctl config set mirror.peers '[http://10.0.0.12:8082]'") { + t.Errorf("%s", out.String()) + } +}