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)
|
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 {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
Reference in New Issue
Block a user