Remade the client in go, now named monitor-agent. The agents now support http basic auth

This commit is contained in:
Kalzu Rekku
2026-07-25 00:01:46 +03:00
parent c98081d360
commit 48bdab08b8
7 changed files with 578 additions and 1608 deletions
+103
View File
@@ -0,0 +1,103 @@
#!/bin/bash
# --- Configuration (Edit these before spreading) ---
TARGET_SERVICE_UUID=""
SERVER_URL="https://monitor.example.com"
AUTH_USER="monitor_user"
AUTH_PASS="monitor_user_pass"
UPDATE_INTERVAL=10
# --- Script Logic ---
# 1. Ensure the script is run as root
if [ "$EUID" -ne 0 ]; then
echo "Please run as root (use sudo)"
exit 1
fi
echo "Starting deployment of Monitoring Agent..."
# 2. Install dependencies (libcap2-bin is needed for setcap)
echo "Installing dependencies..."
apt-get update -qq
apt-get install -y libcap2-bin uuid-runtime -qq
# 3. Create the system user if it doesn't exist
if ! id "monitor-agent" &>/dev/null; then
echo "Creating monitor-agent system user..."
useradd -r -s /bin/false monitor-agent
fi
# 4. Create necessary directories
echo "Creating directories..."
mkdir -p /etc/monitor-agent
mkdir -p /var/lib/monitor-agent
chown -R monitor-agent:monitor-agent /var/lib/monitor-agent
# 5. Install the binary
if [ -f "./agent-go" ]; then
echo "Installing binary to /usr/local/bin..."
cp ./agent-go /usr/local/bin/monitor-agent
chmod +x /usr/local/bin/monitor-agent
# Grant ping capabilities without root
setcap cap_net_raw+ep /usr/local/bin/monitor-agent
else
echo "Error: 'monitor-agent' binary not found in current directory!"
exit 1
fi
# 6. Generate a unique NODE_UUID for this specific server
NEW_UUID=$(uuidgen)
echo "Generated unique Node UUID: $NEW_UUID"
# 7. Create the Environment File
echo "Creating configuration file..."
cat <<EOF > /etc/monitor-agent/agent.env
NODE_UUID=$NEW_UUID
TARGET_SERVICE_UUID=$TARGET_SERVICE_UUID
SERVER_URL=$SERVER_URL
BASIC_AUTH_USERNAME=$AUTH_USER
BASIC_AUTH_PASSWORD=$AUTH_PASS
UPDATE_INTERVAL_SECONDS=$UPDATE_INTERVAL
PEERS_FILE=/var/lib/monitor-agent/known_peers.json
EOF
chmod 600 /etc/monitor-agent/agent.env
chown monitor-agent:monitor-agent /etc/monitor-agent/agent.env
# 8. Create the Systemd Service File
echo "Creating systemd service..."
cat <<EOF > /etc/systemd/system/monitor-agent.service
[Unit]
Description=Node Monitoring Agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=monitor-agent
Group=monitor-agent
WorkingDirectory=/var/lib/monitor-agent
EnvironmentFile=/etc/monitor-agent/agent.env
ExecStart=/usr/local/bin/monitor-agent
CapabilityBoundingSet=CAP_NET_RAW
AmbientCapabilities=CAP_NET_RAW
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
# 9. Start the service
echo "Reloading systemd and starting service..."
systemctl daemon-reload
systemctl enable monitor-agent
systemctl restart monitor-agent
echo "------------------------------------------------"
echo "Deployment Complete!"
echo "Node UUID: $NEW_UUID"
echo "Check status with: systemctl status monitor-agent"
echo "View logs with: journalctl -u monitor-agent -f"
echo "------------------------------------------------"
+19
View File
@@ -0,0 +1,19 @@
module aget_go
go 1.25.0
require (
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus-community/pro-bing v0.9.1 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect
)
+290
View File
@@ -0,0 +1,290 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"runtime"
"strconv"
"sync"
"time"
"github.com/google/uuid"
probing "github.com/prometheus-community/pro-bing"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/host"
"github.com/shirou/gopsutil/v3/load"
"github.com/shirou/gopsutil/v3/mem"
)
// --- Configuration ---
var (
NodeUUID = getEnv("NODE_UUID", uuid.New().String())
TargetServiceUUID = getEnv("TARGET_SERVICE_UUID", "ab73d00a-8169-46bb-997d-f13e5f760973")
BasicAuthUser = getEnv("BASIC_AUTH_USERNAME", "")
BasicAuthPass = getEnv("BASIC_AUTH_PASSWORD", "")
ServerBaseURL = getEnv("SERVER_URL", "https://test.mystaginglab.net")
UpdateInterval = getEnvInt("UPDATE_INTERVAL_SECONDS", 10)
PeersFile = getEnv("PEERS_FILE", fmt.Sprintf("known_peers_%s.json", NodeUUID))
LocalIP = getLocalIP()
KnownPeers = make(map[string]string)
KnownPeersMu sync.RWMutex
)
// --- Structs for JSON ---
type StatusData struct {
UptimeSeconds uint64 `json:"uptime_seconds"`
LoadAvg []float64 `json:"load_avg"`
MemoryUsagePercent float64 `json:"memory_usage_percent"`
}
type Payload struct {
Node string `json:"node"`
Timestamp string `json:"timestamp"`
Status StatusData `json:"status"`
Pings map[string]float64 `json:"pings"`
}
type PeerInfo struct {
IP string `json:"ip"`
LastSeen string `json:"last_seen"`
}
type ServerResponse struct {
Message string `json:"message"`
Peers map[string]PeerInfo `json:"peers"`
}
// --- Helper Functions ---
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
func getEnvInt(key string, fallback int) int {
if value, ok := os.LookupEnv(key); ok {
if i, err := strconv.Atoi(value); err == nil {
return i
}
}
return fallback
}
func getLocalIP() string {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return "127.0.0.1"
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP.String()
}
// --- Peer Persistence ---
func loadPeers() {
KnownPeersMu.Lock()
defer KnownPeersMu.Unlock()
data, err := os.ReadFile(PeersFile)
if err != nil {
log.Printf("No existing peers file found or error reading: %v", err)
return
}
// The Python script handles both {uuid: ip} and {uuid: {ip: ip}}
// We'll decode into a generic map first to handle flexibility
var raw map[string]interface{}
if err := json.Unmarshal(data, &raw); err != nil {
log.Printf("Error decoding peers JSON: %v", err)
return
}
for k, v := range raw {
if ipStr, ok := v.(string); ok {
KnownPeers[k] = ipStr
} else if ipMap, ok := v.(map[string]interface{}); ok {
if ip, ok := ipMap["ip"].(string); ok {
KnownPeers[k] = ip
}
}
}
log.Printf("Loaded %d known peers from %s", len(KnownPeers), PeersFile)
}
func savePeers() {
KnownPeersMu.RLock()
defer KnownPeersMu.RUnlock()
data, err := json.MarshalIndent(KnownPeers, "", " ")
if err != nil {
log.Printf("Error marshaling peers: %v", err)
return
}
if err := os.WriteFile(PeersFile, data, 0644); err != nil {
log.Printf("Error saving peers to file: %v", err)
}
}
// --- Metrics Collection ---
func getSystemMetrics() StatusData {
uptime, _ := host.Uptime()
var loadValues []float64
avg, err := load.Avg()
if err != nil {
// Fallback for Windows/Non-Unix like the Python script
c, _ := cpu.Percent(500*time.Millisecond, false)
cpuUsage := 0.0
if len(c) > 0 {
cpuUsage = c[0] / 100.0
}
loadValues = []float64{cpuUsage, cpuUsage * 0.9, cpuUsage * 0.8}
} else {
loadValues = []float64{avg.Load1, avg.Load5, avg.Load15}
}
v, _ := mem.VirtualMemory()
return StatusData{
UptimeSeconds: uptime,
LoadAvg: loadValues,
MemoryUsagePercent: v.UsedPercent,
}
}
// --- Ping Logic ---
func performPings(targets map[string]string) map[string]float64 {
results := make(map[string]float64)
// Ping self
results[NodeUUID] = runPing(LocalIP)
// Ping peers
for uuid, ip := range targets {
if uuid == NodeUUID {
continue
}
results[uuid] = runPing(ip)
}
return results
}
func runPing(ip string) float64 {
pinger, err := probing.NewPinger(ip)
if err != nil {
return -1.0
}
// On Linux, this requires sudo or 'setcap cap_net_raw+ep'
// If not root, you can try: pinger.SetPrivileged(false) which uses UDP pings
if runtime.GOOS == "windows" {
pinger.SetPrivileged(true)
} else {
pinger.SetPrivileged(false) // Try unprivileged first
}
pinger.Count = 1
pinger.Timeout = 2 * time.Second
err = pinger.Run()
if err != nil {
return -1.0
}
stats := pinger.Statistics()
if stats.PacketsRecv > 0 {
return float64(stats.AvgRtt.Microseconds()) / 1000.0
}
return -1.0
}
// --- Main Loop ---
func main() {
log.Printf("Starting Go Node Client %s", NodeUUID)
log.Printf("Local IP: %s | Server: %s", LocalIP, ServerBaseURL)
loadPeers()
ticker := time.NewTicker(time.Duration(UpdateInterval) * time.Second)
client := &http.Client{Timeout: 15 * time.Second}
for ; ; <-ticker.C {
metrics := getSystemMetrics()
KnownPeersMu.RLock()
targets := make(map[string]string)
for k, v := range KnownPeers {
targets[k] = v
}
KnownPeersMu.RUnlock()
pingResults := performPings(targets)
payload := Payload{
Node: NodeUUID,
Timestamp: time.Now().UTC().Format(time.RFC3339),
Status: metrics,
Pings: pingResults,
}
jsonPayload, _ := json.Marshal(payload)
url := fmt.Sprintf("%s/%s/%s/", ServerBaseURL, TargetServiceUUID, NodeUUID)
req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonPayload))
if err != nil {
log.Printf("Error creating request: %v", err)
continue
}
req.Header.Set("Content-Type", "application/json")
if BasicAuthUser != "" {
auth := BasicAuthUser + ":" + BasicAuthPass
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
}
resp, err := client.Do(req)
if err != nil {
log.Printf("Request failed: %v", err)
continue
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusOK {
var serverResp ServerResponse
if err := json.Unmarshal(body, &serverResp); err == nil {
log.Printf("Update sent. Server: %s", serverResp.Message)
// Update peers
newPeers := make(map[string]string)
for id, info := range serverResp.Peers {
newPeers[id] = info.IP
}
KnownPeersMu.Lock()
KnownPeers = newPeers
KnownPeersMu.Unlock()
savePeers()
}
} else {
log.Printf("Server returned error %d: %s", resp.StatusCode, string(body))
}
}
}