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:
@@ -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 <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 {
|
||||
var sb strings.Builder
|
||||
for _, r := range results {
|
||||
|
||||
@@ -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)
|
||||
|
||||
+2
-10
@@ -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
|
||||
|
||||
+56
-12
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
Reference in New Issue
Block a user