Complete infrastructure platform CLI: - Container runtime (systemd-nspawn) - VoltVisor VMs (Neutron Stardust / QEMU) - Stellarium CAS (content-addressed storage) - ORAS Registry - GitOps integration - Landlock LSM security - Compose orchestration - Mesh networking Copyright (c) Armored Gates LLC. All rights reserved. Licensed under AGPSL v5.0
154 lines
4.3 KiB
Go
154 lines
4.3 KiB
Go
/*
|
|
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
|
|
}
|