feat(node): add Info structure to save every nodes system information

feat(processor): add /info <uuid> command to show basic system info for specific node
This commit is contained in:
2026-08-07 22:31:02 +08:00
parent 56ed32ac0c
commit ebbbe7ba1d
5 changed files with 151 additions and 27 deletions
+62
View File
@@ -65,6 +65,8 @@ func (m *Manager) RouteCommand(cmd Command) (string, error) {
return handleReboot(cmd) return handleReboot(cmd)
case "run": case "run":
return handleRun(cmd) return handleRun(cmd)
case "info":
return handleInfo(cmd)
default: default:
return "", nil return "", nil
} }
@@ -187,6 +189,66 @@ func handleRun(cmd Command) (string, error) {
return template.Render(cfg.ControllerMessage.ServerExecuteResult, params), nil return template.Render(cfg.ControllerMessage.ServerExecuteResult, params), nil
} }
func handleInfo(cmd Command) (string, error) {
if len(cmd.Args) < 1 {
return "Usage: /info <uuid>", nil
}
uuid := cmd.Args[0]
tracker := node.GetTracker()
n, exists := tracker.GetNode(uuid)
if !exists {
return fmt.Sprintf("Server with UUID %s not found", uuid), nil
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Server: %s (%s)\n", n.Name, n.UUID))
if n.Info == nil {
sb.WriteString("No static info available for this server.\n")
return sb.String(), nil
}
sb.WriteString(fmt.Sprintf("OS: %s\n", n.Info.OS.Name))
if n.Info.OS.KernelVersion != "" {
sb.WriteString(fmt.Sprintf("Kernel: %s\n", n.Info.OS.KernelVersion))
}
sb.WriteString(fmt.Sprintf("CPU: %s (%d cores, %s)\n", n.Info.CPU.Model, n.Info.CPU.Cores, n.Info.CPU.Arch))
sb.WriteString(fmt.Sprintf("RAM: %s\n", formatBytes(n.Info.RAM.Total)))
sb.WriteString(fmt.Sprintf("Swap: %s\n", formatBytes(n.Info.SWAP.Total)))
sb.WriteString(fmt.Sprintf("Disk: %s\n", formatBytes(n.Info.Disk.Total)))
if n.Info.BillingCycle != "" {
sb.WriteString(fmt.Sprintf("Billing Cycle: %s\n", n.Info.BillingCycle))
}
if n.Info.Price > 0 {
sb.WriteString(fmt.Sprintf("Price: %.2f\n", n.Info.Price))
}
if n.Info.Group != "" {
sb.WriteString(fmt.Sprintf("Group: %s\n", n.Info.Group))
}
if n.Info.Tags != "" {
sb.WriteString(fmt.Sprintf("Tags: %s\n", n.Info.Tags))
}
return sb.String(), nil
}
// formatBytes renders a byte count in a human-readable form.
func formatBytes(b int64) string {
const (
mb = 1024 * 1024
gb = 1024 * mb
)
switch {
case b >= gb:
return fmt.Sprintf("%.2f GB", float64(b)/float64(gb))
case b >= mb:
return fmt.Sprintf("%.2f MB", float64(b)/float64(mb))
default:
return fmt.Sprintf("%d B", b)
}
}
func formatTaskResults(results []komari.TaskResult) string { func formatTaskResults(results []komari.TaskResult) string {
var sb strings.Builder var sb strings.Builder
for _, r := range results { for _, r := range results {
+30
View File
@@ -13,6 +13,7 @@ import (
"time" "time"
"nukumizu-backend/config" "nukumizu-backend/config"
"nukumizu-backend/internal/node"
"nukumizu-backend/postLog" "nukumizu-backend/postLog"
) )
@@ -205,6 +206,35 @@ func (c *Client) FetchNodes() ([]NodeInfo, error) {
return nodes, nil return nodes, nil
} }
// BuildNodeListData converts the Komari node list into the tracker's input,
// carrying each node's static Info metadata alongside its name. It is used
// whenever the node list is fetched (startup login, reconnect, periodic refresh)
// so the tracker keeps the Info of every node up to date.
func BuildNodeListData(nodes []NodeInfo) map[string]node.NodeListEntry {
entries := make(map[string]node.NodeListEntry, len(nodes))
for _, n := range nodes {
info := &node.Info{}
info.OS.Name = n.OS
info.OS.KernelVersion = n.KernelVersion
info.CPU.Model = n.CPUName
info.CPU.Cores = n.CPUCores
info.CPU.Arch = n.Arch
info.RAM.Total = n.MemTotal
info.SWAP.Total = n.SwapTotal
info.Disk.Total = n.DiskTotal
info.BillingCycle = fmt.Sprintf("%d", n.BillingCycle)
info.Price = n.Price
info.Group = n.Group
info.Tags = n.Tags
entries[n.UUID] = node.NodeListEntry{
Name: n.Name,
Info: info,
}
}
return entries
}
// FetchRecent retrieves the recent status for a specific node. // FetchRecent retrieves the recent status for a specific node.
func (c *Client) FetchRecent(uuid string) (json.RawMessage, error) { func (c *Client) FetchRecent(uuid string) (json.RawMessage, error) {
req, err := http.NewRequest("GET", c.baseURL+"/api/recent/"+url.PathEscape(uuid), nil) req, err := http.NewRequest("GET", c.baseURL+"/api/recent/"+url.PathEscape(uuid), nil)
+2 -10
View File
@@ -350,11 +350,7 @@ func (w *WSClient) tryReconnect() {
} else { } else {
tracker := node.GetTracker() tracker := node.GetTracker()
if tracker != nil { if tracker != nil {
nodeNames := make(map[string]string, len(nodes)) tracker.UpdateNodeList(BuildNodeListData(nodes))
for _, n := range nodes {
nodeNames[n.UUID] = n.Name
}
tracker.UpdateNodeList(nodeNames)
} }
} }
} }
@@ -431,11 +427,7 @@ func LoginAndStart() error {
tracker := node.GetTracker() tracker := node.GetTracker()
if tracker != nil { if tracker != nil {
nodeNames := make(map[string]string, len(nodes)) tracker.UpdateNodeList(BuildNodeListData(nodes))
for _, n := range nodes {
nodeNames[n.UUID] = n.Name
}
tracker.UpdateNodeList(nodeNames)
} }
return nil return nil
+56 -12
View File
@@ -46,6 +46,38 @@ type Report struct {
UpdatedAt string `json:"updated_at"` UpdatedAt string `json:"updated_at"`
} }
type Info struct {
OS struct {
Name string `json:"os"`
KernelVersion string `json:"kernel_version"`
} `json:"os"`
CPU struct {
Model string `json:"cpu"`
Cores int `json:"cpu_cores"`
Arch string `json:"arch"`
} `json:"cpu"`
RAM struct {
Total int64 `json:"mem_total"`
} `json:"ram"`
SWAP struct {
Total int64 `json:"swap_total"`
} `json:"swap"`
Disk struct {
Total int64 `json:"disk_total"`
} `json:"disk"`
BillingCycle string `json:"billing_cycle"`
Price float64 `json:"price"`
Group string `json:"group"`
Tags string `json:"tags"`
}
// NodeListEntry is the static metadata of a node from the Komari node list,
// passed to the tracker alongside the status data.
type NodeListEntry struct {
Name string
Info *Info
}
// Node holds all tracked information about a single server node. // Node holds all tracked information about a single server node.
type Node struct { type Node struct {
UUID string UUID string
@@ -53,6 +85,7 @@ type Node struct {
Online bool Online bool
LatestReport *Report LatestReport *Report
LastUpdated time.Time LastUpdated time.Time
Info *Info
} }
// StatusChange represents a node status transition. // StatusChange represents a node status transition.
@@ -126,36 +159,39 @@ func (t *Tracker) fireCallbacks(change StatusChange) {
} }
} }
// UpdateNodeList replaces the full node list and updates the uuid→name map. // UpdateNodeList replaces the full node list and updates each node's name and
// nodeNames maps UUID → node name from the Komari node list. // static Info metadata. entries maps UUID → node metadata from the Komari node
func (t *Tracker) UpdateNodeList(nodeNames map[string]string) { // list.
func (t *Tracker) UpdateNodeList(entries map[string]NodeListEntry) {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
// The node list defines the set of nodes to wait for during the initial // The node list defines the set of nodes to wait for during the initial
// status refresh. // status refresh.
t.knownUUIDs = make(map[string]bool, len(nodeNames)) t.knownUUIDs = make(map[string]bool, len(entries))
for uuid := range nodeNames { for uuid := range entries {
t.knownUUIDs[uuid] = true t.knownUUIDs[uuid] = true
} }
t.uuidToName = make(map[string]string, len(nodeNames)) t.uuidToName = make(map[string]string, len(entries))
for uuid, name := range nodeNames { for uuid, entry := range entries {
t.uuidToName[uuid] = name t.uuidToName[uuid] = entry.Name
// Ensure node entry exists. // Ensure node entry exists.
if _, exists := t.nodes[uuid]; !exists { if _, exists := t.nodes[uuid]; !exists {
t.nodes[uuid] = &Node{ t.nodes[uuid] = &Node{
UUID: uuid, UUID: uuid,
Name: name, Name: entry.Name,
Online: false, Online: false,
Info: entry.Info,
} }
} else { } else {
// Update name in case it changed. // Update name/info in case they changed.
t.nodes[uuid].Name = name t.nodes[uuid].Name = entry.Name
t.nodes[uuid].Info = entry.Info
} }
} }
// postLog.Debug(fmt.Sprintf("Node list updated: %d nodes", len(nodeNames))) // postLog.Debug(fmt.Sprintf("Node list updated: %d nodes", len(entries)))
} }
// UpdateStatus processes a WebSocket status update from Komari. // UpdateStatus processes a WebSocket status update from Komari.
@@ -375,6 +411,10 @@ func (t *Tracker) GetNode(uuid string) (*Node, bool) {
reportCopy := *node.LatestReport reportCopy := *node.LatestReport
nodeCopy.LatestReport = &reportCopy nodeCopy.LatestReport = &reportCopy
} }
if node.Info != nil {
infoCopy := *node.Info
nodeCopy.Info = &infoCopy
}
return &nodeCopy, true return &nodeCopy, true
} }
@@ -389,6 +429,10 @@ func (t *Tracker) GetAllNodes() []*Node {
reportCopy := *node.LatestReport reportCopy := *node.LatestReport
nodeCopy.LatestReport = &reportCopy nodeCopy.LatestReport = &reportCopy
} }
if node.Info != nil {
infoCopy := *node.Info
nodeCopy.Info = &infoCopy
}
result = append(result, &nodeCopy) result = append(result, &nodeCopy)
} }
return result return result
+1 -5
View File
@@ -261,11 +261,7 @@ func startBackgroundTasks(_ *config.Config) {
} }
tracker := node.GetTracker() tracker := node.GetTracker()
if tracker != nil { if tracker != nil {
nodeNames := make(map[string]string, len(nodes)) tracker.UpdateNodeList(komari.BuildNodeListData(nodes))
for _, n := range nodes {
nodeNames[n.UUID] = n.Name
}
tracker.UpdateNodeList(nodeNames)
} }
} }
}() }()