Everything in server.yaml that is not one machine's own is now a versioned document in the store (cluster_settings), the same on every control plane. The first start against a store imports the file's cluster keys, with the baseline guard policy when there is none; after that those keys in a file are ignored, with a warning. Each control plane polls the document's version and its own server.yaml's hash. A change is validated and applied: guard live (Gate.Replace), anything else by a graceful restart that systemd brings back, standbys first and the leader 30s later. A version that does not load on a machine is not applied; at start the newest one that loads is used, so a bad edit cannot keep every control plane down. llmgw ctl config show/get/set/unset/edit/history/rollback, the admin API under /api/v1/settings, and the console's Settings screen, which layers the store's document over the machine's file, write it. Commands beside the control plane that read cluster keys (init, node add, server add, backup, upgrade) read the layered configuration; init --bundle writes offline to the store once it holds the settings. A standby that becomes the leader re-reads models, engine choices and facility holds from the store. Retires appendGuardIfMissing: the import does it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
125 lines
3.7 KiB
Go
125 lines
3.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/config"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/firewall"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/pgha"
|
|
)
|
|
|
|
// ensureFirewall keeps this control plane's internal ports to the networks
|
|
// the cluster is on: the agents' stream, the metrics, the mirror's files,
|
|
// the engines and etcd when they are here. The client API is not among
|
|
// them; it is meant to be reached.
|
|
func (in *serverInit) ensureFirewall(ctx context.Context) error {
|
|
cfg, err := loadEffectiveConfig(in.configPath())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ports := controlPlanePorts(cfg)
|
|
if !in.noMirror && in.mirrorURL == "" {
|
|
ports = append(ports, firewall.MirrorFiles)
|
|
}
|
|
if !in.noNode {
|
|
from, to := cfg.EnginePorts()
|
|
ports = append(ports, firewall.Engines(from, to), firewall.NodeExporter)
|
|
}
|
|
if _, err := os.Stat("/etc/default/etcd"); err == nil {
|
|
// The store's third etcd vote.
|
|
ports = append(ports, firewall.EtcdClient, firewall.EtcdPeer)
|
|
}
|
|
self, err := ipv4Of(in.reachableAddr())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nets, err := firewall.Networks(ctx, pgha.Local{}, []string{self})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
extra, _ := cfg.FirewallAllow()
|
|
nets = append(nets, extra...)
|
|
changed, err := firewall.Install(ctx, pgha.Local{}, firewall.Spec{Ports: ports, Allow: nets})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
shown := make([]string, len(nets))
|
|
for i, n := range nets {
|
|
shown[i] = n.String()
|
|
}
|
|
if changed {
|
|
in.done("firewall: llmgw's internal ports answer %s only (%s)", strings.Join(shown, ", "), firewall.RulesFile)
|
|
} else {
|
|
in.kept("firewall: llmgw's internal ports answer %s only", strings.Join(shown, ", "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// storeFirewall keeps each store member's database, Patroni and etcd ports to
|
|
// the networks it reaches this control plane and the other members on. A
|
|
// member it cannot be set on is said, and left as it was.
|
|
func (in *serverInit) storeFirewall(ctx context.Context, members []pgha.Node, self string, extra []netip.Prefix) {
|
|
ports := []firewall.Port{firewall.Postgres, firewall.PatroniAPI, firewall.EtcdClient, firewall.EtcdPeer,
|
|
firewall.NodeExporter}
|
|
for _, m := range members {
|
|
toward := []string{self}
|
|
for _, o := range members {
|
|
if o.IP != m.IP {
|
|
toward = append(toward, o.IP)
|
|
}
|
|
}
|
|
nets, err := firewall.Networks(ctx, m.Remote, toward)
|
|
if err == nil {
|
|
nets = append(nets, extra...)
|
|
var changed bool
|
|
if changed, err = firewall.Install(ctx, m.Remote, firewall.Spec{Ports: ports, Allow: nets}); err == nil {
|
|
if changed {
|
|
in.done("firewall on %s: the store's ports answer the cluster's networks only", m.Name)
|
|
} else {
|
|
in.kept("firewall on %s", m.Name)
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
fmt.Fprintf(in.out, " !! firewall on %s: %v\n", m.Name, err)
|
|
}
|
|
}
|
|
|
|
// controlPlanePorts are a control plane's internal ports, from its
|
|
// configuration: the agents' stream, and the metrics when they listen
|
|
// beyond this machine.
|
|
func controlPlanePorts(cfg config.Server) []firewall.Port {
|
|
var ports []firewall.Port
|
|
if p := portOf(cfg.AgentListen); p > 0 {
|
|
ports = append(ports, firewall.Single(p, "control plane: the agents' stream"))
|
|
}
|
|
if p := portOf(cfg.MetricsListen); p > 0 && !loopbackListen(cfg.MetricsListen) {
|
|
ports = append(ports, firewall.Single(p, "control plane: metrics, for Prometheus"))
|
|
}
|
|
return ports
|
|
}
|
|
|
|
func portOf(listen string) int {
|
|
_, port, err := net.SplitHostPort(listen)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
p, _ := strconv.Atoi(port)
|
|
return p
|
|
}
|
|
|
|
func loopbackListen(listen string) bool {
|
|
host, _, err := net.SplitHostPort(listen)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
ip := net.ParseIP(host)
|
|
return host == "localhost" || (ip != nil && ip.IsLoopback())
|
|
}
|