/* Volt Cluster — Node agent for worker nodes. The node agent runs on every worker and is responsible for: - Sending heartbeats to the control plane - Reporting resource usage (CPU, memory, disk, workload count) - Accepting workload scheduling commands from the control plane - Executing workload lifecycle operations locally Communication with the control plane uses HTTPS over the mesh network. Copyright (c) Armored Gates LLC. All rights reserved. AGPSL v5 — Source-available. Anti-competition clauses apply. */ package cluster import ( "fmt" "os" "os/exec" "runtime" "strconv" "strings" "time" ) // NodeAgent runs on worker nodes and communicates with the control plane. type NodeAgent struct { nodeID string nodeName string controlURL string interval time.Duration stopCh chan struct{} } // NewNodeAgent creates a node agent for the given cluster state. func NewNodeAgent(state *ClusterState) *NodeAgent { interval := state.HeartbeatInterval if interval == 0 { interval = DefaultHeartbeatInterval } return &NodeAgent{ nodeID: state.NodeID, nodeName: state.NodeName, controlURL: state.ControlURL, interval: interval, stopCh: make(chan struct{}), } } // CollectResources gathers current node resource information. func CollectResources() NodeResources { res := NodeResources{ CPUCores: runtime.NumCPU(), } // Memory from /proc/meminfo if data, err := os.ReadFile("/proc/meminfo"); err == nil { lines := strings.Split(string(data), "\n") for _, line := range lines { if strings.HasPrefix(line, "MemTotal:") { res.MemoryTotalMB = parseMemInfoKB(line) / 1024 } else if strings.HasPrefix(line, "MemAvailable:") { availMB := parseMemInfoKB(line) / 1024 res.MemoryUsedMB = res.MemoryTotalMB - availMB } } } // Disk usage from df if out, err := exec.Command("df", "--output=size,used", "-BG", "/").Output(); err == nil { lines := strings.Split(strings.TrimSpace(string(out)), "\n") if len(lines) >= 2 { fields := strings.Fields(lines[1]) if len(fields) >= 2 { res.DiskTotalGB = parseGB(fields[0]) res.DiskUsedGB = parseGB(fields[1]) } } } // Container count from machinectl if out, err := exec.Command("machinectl", "list", "--no-legend", "--no-pager").Output(); err == nil { count := 0 for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { if strings.TrimSpace(line) != "" { count++ } } res.ContainerCount = count } // Workload count from volt state if data, err := os.ReadFile("/var/lib/volt/workload-state.json"); err == nil { // Quick count of workload entries count := strings.Count(string(data), `"id"`) res.WorkloadCount = count } return res } // GetSystemInfo returns OS and kernel information. func GetSystemInfo() (osInfo, kernelVersion string) { if out, err := exec.Command("uname", "-r").Output(); err == nil { kernelVersion = strings.TrimSpace(string(out)) } if data, err := os.ReadFile("/etc/os-release"); err == nil { for _, line := range strings.Split(string(data), "\n") { if strings.HasPrefix(line, "PRETTY_NAME=") { osInfo = strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"") break } } } return } // FormatResources returns a human-readable resource summary. func FormatResources(r NodeResources) string { memPct := float64(0) if r.MemoryTotalMB > 0 { memPct = float64(r.MemoryUsedMB) / float64(r.MemoryTotalMB) * 100 } diskPct := float64(0) if r.DiskTotalGB > 0 { diskPct = float64(r.DiskUsedGB) / float64(r.DiskTotalGB) * 100 } return fmt.Sprintf("CPU: %d cores | RAM: %dMB/%dMB (%.0f%%) | Disk: %dGB/%dGB (%.0f%%) | Containers: %d", r.CPUCores, r.MemoryUsedMB, r.MemoryTotalMB, memPct, r.DiskUsedGB, r.DiskTotalGB, diskPct, r.ContainerCount, ) } // ── Helpers ────────────────────────────────────────────────────────────────── func parseMemInfoKB(line string) int64 { // Format: "MemTotal: 16384000 kB" fields := strings.Fields(line) if len(fields) >= 2 { val, _ := strconv.ParseInt(fields[1], 10, 64) return val } return 0 } func parseGB(s string) int64 { s = strings.TrimSuffix(s, "G") val, _ := strconv.ParseInt(s, 10, 64) return val }