CI / test (push) Failing after 7m1s
slurm.shared mounts the site's NFS export, has an llmgw node export the path over NFS to the Slurm machines alone (2049 in the firewall), or looks for a file system the site mounts. slurm.users "llmgw" makes each active account a Linux user on every Slurm machine: login from its e-mail, a UID from 200000 kept in slurm_users and never reused, its home on the shared path, its SSH keys from the portal's Profile (or ctl slurm users keys) read by sshd from /etc/llmgw/ssh-keys. server init --slurm chooses /shared from the first machine and llmgw's users; --slurm-shared and --slurm-users change that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
128 lines
4.0 KiB
Go
128 lines
4.0 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"net/netip"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
llmgwv1 "gitea.mynodes.xyz/ilker/llmgw/api/gen/llmgw/v1"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/firewall"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/slurm"
|
|
)
|
|
|
|
// slurmPorts are Slurm's on a node that runs any of it: slurmctld's and
|
|
// slurmd's, and srun's range for the steps it starts.
|
|
var slurmPorts = []firewall.Port{
|
|
{From: slurm.CtldPort, To: slurm.DbdPort, What: "slurm: slurmctld, slurmd and slurmdbd"},
|
|
{From: slurm.SrunPortsFrom, To: slurm.SrunPortsTo, What: "slurm: srun, for the steps it starts"},
|
|
}
|
|
|
|
// nfsPort is NFSv4's, on the machine that exports the Slurm cluster's
|
|
// shared files.
|
|
var nfsPort = firewall.Port{From: slurm.NFSPort, To: slurm.NFSPort, What: "nfs: the Slurm cluster's shared files"}
|
|
|
|
// applyFirewall keeps llmgw's firewall on this node as the cluster's
|
|
// settings say: the engines' ports and the node exporter's for the networks
|
|
// the node reaches its control planes on, and those named, or no table at
|
|
// all. Off the receive loop, one at a time.
|
|
func (a *Agent) applyFirewall(m *llmgwv1.Firewall) {
|
|
a.fwMu.Lock()
|
|
defer a.fwMu.Unlock()
|
|
a.fwLast = m
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
defer cancel()
|
|
r := a.fwRunner
|
|
if r == nil {
|
|
r = firewall.Local{}
|
|
}
|
|
if !m.GetEnabled() {
|
|
had, err := firewall.Disable(ctx, r)
|
|
switch {
|
|
case err != nil:
|
|
a.log.Warn("could not take llmgw's firewall off this node", "err", err)
|
|
case had:
|
|
a.log.Info("llmgw's firewall is off on this node, as the cluster's settings say")
|
|
}
|
|
return
|
|
}
|
|
a.mu.Lock()
|
|
servers := append([]string(nil), a.serverList...)
|
|
a.mu.Unlock()
|
|
nets, err := firewall.Networks(ctx, r, controlPlaneIPs(ctx, servers))
|
|
if err != nil {
|
|
a.log.Warn("could not tell which networks reach the control planes; the firewall is left as it is", "err", err)
|
|
return
|
|
}
|
|
for _, n := range m.GetNetworks() {
|
|
if p, err := netip.ParsePrefix(n); err == nil {
|
|
nets = append(nets, p)
|
|
} else if ad, err := netip.ParseAddr(n); err == nil {
|
|
nets = append(nets, netip.PrefixFrom(ad, ad.BitLen()))
|
|
}
|
|
}
|
|
from, to := int(m.GetEnginePortsFrom()), int(m.GetEnginePortsTo())
|
|
ports := []firewall.Port{firewall.NodeExporter}
|
|
if from > 0 && to >= from {
|
|
ports = append(ports, firewall.Engines(from, to))
|
|
}
|
|
// Once there, they stay: the ruleset keeps every port it held, so a
|
|
// node that leaves Slurm keeps them closed to everyone but the cluster.
|
|
if a.slurm != nil && a.slurm.HasPart() {
|
|
ports = append(ports, slurmPorts...)
|
|
}
|
|
if a.slurm != nil && a.slurm.Serves() {
|
|
ports = append(ports, nfsPort)
|
|
}
|
|
changed, err := firewall.Install(ctx, r, firewall.Spec{Ports: ports, Allow: nets})
|
|
switch {
|
|
case err != nil:
|
|
a.log.Warn("could not put llmgw's firewall on this node", "err", err)
|
|
case changed:
|
|
a.log.Info("llmgw's firewall is on this node", "networks", len(nets))
|
|
}
|
|
}
|
|
|
|
// slurmMessage is the leader's word on this node's part in Slurm. A node
|
|
// that gets one, or starts or stops exporting the shared files, has the
|
|
// firewall applied again, with Slurm's ports and NFS's.
|
|
func (a *Agent) slurmMessage(m *llmgwv1.Slurm) {
|
|
had, served := a.slurm.HasPart(), a.slurm.Serves()
|
|
a.slurm.Apply(m)
|
|
if (had || !a.slurm.HasPart()) && served == a.slurm.Serves() {
|
|
return
|
|
}
|
|
a.fwMu.Lock()
|
|
last := a.fwLast
|
|
a.fwMu.Unlock()
|
|
if last != nil && last.GetEnabled() {
|
|
go a.applyFirewall(last)
|
|
}
|
|
}
|
|
|
|
// controlPlaneIPs are the addresses of the control planes an agent dials.
|
|
func controlPlaneIPs(ctx context.Context, servers []string) []string {
|
|
var out []string
|
|
for _, s := range servers {
|
|
host := s
|
|
if u, err := url.Parse(s); err == nil && u.Host != "" {
|
|
host = u.Hostname()
|
|
}
|
|
host = strings.Trim(host, "[]")
|
|
if a, err := netip.ParseAddr(host); err == nil {
|
|
out = append(out, a.String())
|
|
continue
|
|
}
|
|
ips, err := net.DefaultResolver.LookupNetIP(ctx, "ip", host)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, ip := range ips {
|
|
out = append(out, ip.Unmap().String())
|
|
}
|
|
}
|
|
return out
|
|
}
|