diff --git a/internal/cli/controlplanes.go b/internal/cli/controlplanes.go index f532fed0..95209836 100644 --- a/internal/cli/controlplanes.go +++ b/internal/cli/controlplanes.go @@ -1,7 +1,6 @@ package cli import ( - "bufio" "context" "net" "net/http" @@ -15,8 +14,8 @@ import ( "gitea.mynodes.xyz/ilker/llmgw/internal/store" ) -// keepalivedPath is where `server add --vip` writes the floating address. -const keepalivedPath = "/etc/keepalived/keepalived.conf" +// floatingPath is where `server add --vip` records the floating address. +const floatingPath = "/etc/llmgw/floating" // controlPlaneProbe answers the summary's question of which control plane // leads: it asks each one's /leader, on the client port every one of them @@ -46,7 +45,7 @@ func newControlPlaneProbe(st *store.DB, listen string) *controlPlaneProbe { self, _ := os.Hostname() return &controlPlaneProbe{st: st, self: self, leaderURL: func(host string) string { return "http://" + net.JoinHostPort(host, port) + "/leader" }, - floating: floatingAddress(keepalivedPath), + floating: floatingAddress(floatingPath), client: &http.Client{Timeout: time.Second}} } @@ -104,30 +103,18 @@ func (p *controlPlaneProbe) ask(ctx context.Context, host string) string { return "not answering" } -// floatingAddress is the address in the keepalived configuration server add -// wrote, without its prefix; "" when there is none, or the file is someone -// else's. +// floatingAddress is the address in the file server add keeps beside the +// keepalived configuration, without its prefix; "" when nothing floats. +// keepalived's own file holds its password and is root's alone, and this +// process is not root. func floatingAddress(path string) string { - f, err := os.Open(path) //nolint:gosec // a fixed system path + b, err := os.ReadFile(path) //nolint:gosec // a fixed path of our own if err != nil { return "" } - defer f.Close() - sc := bufio.NewScanner(f) - ours, in := false, false - for sc.Scan() { - line := strings.TrimSpace(sc.Text()) - switch { - case strings.HasPrefix(line, "# llmgw:"): - ours = true - case strings.HasPrefix(line, "virtual_ipaddress"): - in = true - case in && line == "}": - return "" - case in && ours && line != "": - addr, _, _ := strings.Cut(strings.Fields(line)[0], "/") - return addr - } + addr, _, _ := strings.Cut(strings.TrimSpace(string(b)), "/") + if net.ParseIP(addr) == nil { + return "" } - return "" + return addr } diff --git a/internal/cli/controlplanes_test.go b/internal/cli/controlplanes_test.go index 179f5c6e..7fde4e61 100644 --- a/internal/cli/controlplanes_test.go +++ b/internal/cli/controlplanes_test.go @@ -111,19 +111,18 @@ func TestOneControlPlaneIsNotASetOfThem(t *testing.T) { func TestTheFloatingAddressIsReadFromWhatServerAddWrote(t *testing.T) { dir := t.TempDir() - ours := filepath.Join(dir, "ours.conf") - if err := os.WriteFile(ours, []byte(keepalivedConf("eth0", "10.0.0.161", 24, 110, "abcd1234")), 0o600); err != nil { + file := filepath.Join(dir, "floating") + if err := os.WriteFile(file, []byte("10.0.0.161/24\n"), 0o644); err != nil { t.Fatal(err) } - if got := floatingAddress(ours); got != "10.0.0.161" { + if got := floatingAddress(file); got != "10.0.0.161" { t.Errorf("read %q", got) } - theirs := filepath.Join(dir, "theirs.conf") - if err := os.WriteFile(theirs, []byte("vrrp_instance x {\n virtual_ipaddress {\n 10.9.9.9/24\n }\n}\n"), 0o600); err != nil { + if err := os.WriteFile(file, []byte("not an address\n"), 0o644); err != nil { t.Fatal(err) } - if got := floatingAddress(theirs); got != "" { - t.Errorf("someone else's keepalived read as ours: %q", got) + if got := floatingAddress(file); got != "" { + t.Errorf("nonsense read as %q", got) } if got := floatingAddress(filepath.Join(dir, "none")); got != "" { t.Errorf("no file read as %q", got) diff --git a/internal/cli/serveradd.go b/internal/cli/serveradd.go index 004a5daa..5bae60f0 100644 --- a/internal/cli/serveradd.go +++ b/internal/cli/serveradd.go @@ -489,6 +489,12 @@ sudo systemctl restart keepalived`, []byte(conf)); err != nil { return fmt.Errorf("on %s: %v: %s", m.name, err, strings.TrimSpace(string(out))) } } + // Where the summary reads it, on both: readable, unlike keepalived's. + for _, r := range []onboard.Remote{here, sa.remote} { + if out, err := r.Run(ctx, "sudo sh -c 'cat > "+floatingPath+" && chmod 0644 "+floatingPath+"'", []byte(sa.vip+"\n")); err != nil { + return fmt.Errorf("recording it: %v: %s", err, strings.TrimSpace(string(out))) + } + } sa.done("%s follows whichever control plane leads (keepalived on both)", sa.vip) return nil }