server add gives another machine this llmgw, a certificate, the configuration and store address, lets it into the store through Patroni, and starts it; --vip keeps a floating address on the leader with keepalived, following a new /leader endpoint. Nodes learn every control plane from the leader and notice a dead one by HTTP/2 ping instead of after the kernel's fifteen minutes. The setup, console, node and init screens are drawn full screen in one frame: title bar, sidebar, bordered panel, key bar, dialogs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
78 lines
2.1 KiB
Go
78 lines
2.1 KiB
Go
package agent
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
|
|
llmgwv1 "gitea.mynodes.xyz/ilker/llmgw/api/gen/llmgw/v1"
|
|
)
|
|
|
|
// controlPlanesFile keeps the control planes the leader last named, beside
|
|
// the agent's configuration rather than in it: the configuration is the
|
|
// operator's, and a list rewritten in it would be a file nobody wrote.
|
|
var controlPlanesFile = "/var/lib/llmgw-agent/control-planes.json"
|
|
|
|
// servers is every address to dial, in the order to try them: the
|
|
// configuration's, then any the leader named that it lacks.
|
|
func (a *Agent) servers() []string {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
if len(a.serverList) == 0 {
|
|
return a.cfg.Servers
|
|
}
|
|
return a.serverList
|
|
}
|
|
|
|
// mergeServers puts the leader's list after the configured one, without
|
|
// repeats, and says whether that changed anything.
|
|
func mergeServers(configured, told []string, secure bool) []string {
|
|
out := slices.Clone(configured)
|
|
for _, s := range told {
|
|
fixed, err := normalizeOne(s, secure)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if !slices.Contains(out, fixed) {
|
|
out = append(out, fixed)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// setControlPlanes takes the leader's list: used from the next reconnect,
|
|
// and kept for the next start.
|
|
func (a *Agent) setControlPlanes(m *llmgwv1.ControlPlanes) {
|
|
told := m.GetServers()
|
|
merged := mergeServers(a.cfg.Servers, told, a.cfg.TLS.Enabled())
|
|
a.mu.Lock()
|
|
changed := !slices.Equal(merged, a.serverList)
|
|
a.serverList = merged
|
|
a.mu.Unlock()
|
|
if !changed {
|
|
return
|
|
}
|
|
a.log.Info("the control planes to dial", "servers", merged)
|
|
b, _ := json.Marshal(told)
|
|
if err := os.MkdirAll(filepath.Dir(controlPlanesFile), 0o755); err == nil {
|
|
tmp := controlPlanesFile + ".new"
|
|
if err := os.WriteFile(tmp, b, 0o644); err == nil {
|
|
_ = os.Rename(tmp, controlPlanesFile)
|
|
}
|
|
}
|
|
}
|
|
|
|
// loadControlPlanes is the list kept from the last time the leader named it.
|
|
func loadControlPlanes(configured []string, secure bool) []string {
|
|
b, err := os.ReadFile(controlPlanesFile)
|
|
if err != nil {
|
|
return configured
|
|
}
|
|
var told []string
|
|
if json.Unmarshal(b, &told) != nil {
|
|
return configured
|
|
}
|
|
return mergeServers(configured, told, secure)
|
|
}
|