From ebbbe7ba1d7d7ab18b56b93860b61270bd3b2d3e Mon Sep 17 00:00:00 2001 From: NanamiAdmin Date: Fri, 7 Aug 2026 22:31:02 +0800 Subject: [PATCH] feat(node): add Info structure to save every nodes system information feat(processor): add /info command to show basic system info for specific node --- internal/controller/processor.go | 62 +++++++++++++++++++++++++++++ internal/komari/client.go | 30 ++++++++++++++ internal/komari/ws.go | 12 +----- internal/node/tracker.go | 68 ++++++++++++++++++++++++++------ main.go | 6 +-- 5 files changed, 151 insertions(+), 27 deletions(-) diff --git a/internal/controller/processor.go b/internal/controller/processor.go index 9dcc5e9..d6eace0 100644 --- a/internal/controller/processor.go +++ b/internal/controller/processor.go @@ -65,6 +65,8 @@ func (m *Manager) RouteCommand(cmd Command) (string, error) { return handleReboot(cmd) case "run": return handleRun(cmd) + case "info": + return handleInfo(cmd) default: return "", nil } @@ -187,6 +189,66 @@ func handleRun(cmd Command) (string, error) { return template.Render(cfg.ControllerMessage.ServerExecuteResult, params), nil } +func handleInfo(cmd Command) (string, error) { + if len(cmd.Args) < 1 { + return "Usage: /info ", 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 { var sb strings.Builder for _, r := range results { diff --git a/internal/komari/client.go b/internal/komari/client.go index a85627c..6bad168 100644 --- a/internal/komari/client.go +++ b/internal/komari/client.go @@ -13,6 +13,7 @@ import ( "time" "nukumizu-backend/config" + "nukumizu-backend/internal/node" "nukumizu-backend/postLog" ) @@ -205,6 +206,35 @@ func (c *Client) FetchNodes() ([]NodeInfo, error) { 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. func (c *Client) FetchRecent(uuid string) (json.RawMessage, error) { req, err := http.NewRequest("GET", c.baseURL+"/api/recent/"+url.PathEscape(uuid), nil) diff --git a/internal/komari/ws.go b/internal/komari/ws.go index 1a16a6d..850e49e 100644 --- a/internal/komari/ws.go +++ b/internal/komari/ws.go @@ -350,11 +350,7 @@ func (w *WSClient) tryReconnect() { } else { tracker := node.GetTracker() if tracker != nil { - nodeNames := make(map[string]string, len(nodes)) - for _, n := range nodes { - nodeNames[n.UUID] = n.Name - } - tracker.UpdateNodeList(nodeNames) + tracker.UpdateNodeList(BuildNodeListData(nodes)) } } } @@ -431,11 +427,7 @@ func LoginAndStart() error { tracker := node.GetTracker() if tracker != nil { - nodeNames := make(map[string]string, len(nodes)) - for _, n := range nodes { - nodeNames[n.UUID] = n.Name - } - tracker.UpdateNodeList(nodeNames) + tracker.UpdateNodeList(BuildNodeListData(nodes)) } return nil diff --git a/internal/node/tracker.go b/internal/node/tracker.go index 743efc1..a20f72b 100644 --- a/internal/node/tracker.go +++ b/internal/node/tracker.go @@ -46,6 +46,38 @@ type Report struct { 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. type Node struct { UUID string @@ -53,6 +85,7 @@ type Node struct { Online bool LatestReport *Report LastUpdated time.Time + Info *Info } // 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. -// nodeNames maps UUID → node name from the Komari node list. -func (t *Tracker) UpdateNodeList(nodeNames map[string]string) { +// UpdateNodeList replaces the full node list and updates each node's name and +// static Info metadata. entries maps UUID → node metadata from the Komari node +// list. +func (t *Tracker) UpdateNodeList(entries map[string]NodeListEntry) { t.mu.Lock() defer t.mu.Unlock() // The node list defines the set of nodes to wait for during the initial // status refresh. - t.knownUUIDs = make(map[string]bool, len(nodeNames)) - for uuid := range nodeNames { + t.knownUUIDs = make(map[string]bool, len(entries)) + for uuid := range entries { t.knownUUIDs[uuid] = true } - t.uuidToName = make(map[string]string, len(nodeNames)) - for uuid, name := range nodeNames { - t.uuidToName[uuid] = name + t.uuidToName = make(map[string]string, len(entries)) + for uuid, entry := range entries { + t.uuidToName[uuid] = entry.Name // Ensure node entry exists. if _, exists := t.nodes[uuid]; !exists { t.nodes[uuid] = &Node{ UUID: uuid, - Name: name, + Name: entry.Name, Online: false, + Info: entry.Info, } } else { - // Update name in case it changed. - t.nodes[uuid].Name = name + // Update name/info in case they changed. + 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. @@ -375,6 +411,10 @@ func (t *Tracker) GetNode(uuid string) (*Node, bool) { reportCopy := *node.LatestReport nodeCopy.LatestReport = &reportCopy } + if node.Info != nil { + infoCopy := *node.Info + nodeCopy.Info = &infoCopy + } return &nodeCopy, true } @@ -389,6 +429,10 @@ func (t *Tracker) GetAllNodes() []*Node { reportCopy := *node.LatestReport nodeCopy.LatestReport = &reportCopy } + if node.Info != nil { + infoCopy := *node.Info + nodeCopy.Info = &infoCopy + } result = append(result, &nodeCopy) } return result diff --git a/main.go b/main.go index e8724c4..d633452 100644 --- a/main.go +++ b/main.go @@ -261,11 +261,7 @@ func startBackgroundTasks(_ *config.Config) { } tracker := node.GetTracker() if tracker != nil { - nodeNames := make(map[string]string, len(nodes)) - for _, n := range nodes { - nodeNames[n.UUID] = n.Name - } - tracker.UpdateNodeList(nodeNames) + tracker.UpdateNodeList(komari.BuildNodeListData(nodes)) } } }()