Files
CLICD/backend/internal/api/host.go
T
MengMengCode e306d2d06b first commit
2026-06-05 19:23:28 +08:00

377 lines
8.3 KiB
Go

package api
import (
"bufio"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"time"
"clicd/internal/lxc"
)
type HostInfo struct {
CPU CpuInfo `json:"cpu"`
RAM MemoryInfo `json:"ram"`
Disk DiskInfo `json:"disk"`
Network NetworkInfo `json:"network"`
DiskIO DiskIOInfo `json:"disk_io"`
Load LoadInfo `json:"load"`
}
type LoadInfo struct {
Load1 float64 `json:"load1"`
Load5 float64 `json:"load5"`
Load15 float64 `json:"load15"`
}
type CpuInfo struct {
Cores int `json:"cores"`
Usage float64 `json:"usage_pct"`
}
type MemoryInfo struct {
TotalMB int64 `json:"total_mb"`
UsedMB int64 `json:"used_mb"`
FreeMB int64 `json:"free_mb"`
}
type DiskInfo struct {
TotalGB float64 `json:"total_gb"`
UsedGB float64 `json:"used_gb"`
FreeGB float64 `json:"free_gb"`
}
type NetworkInfo struct {
RXBytes uint64 `json:"rx_bytes"`
TXBytes uint64 `json:"tx_bytes"`
RXBps float64 `json:"rx_bps"`
TXBps float64 `json:"tx_bps"`
PublicIPv4 string `json:"public_ipv4"`
PublicIPv4Interface string `json:"public_ipv4_interface"`
PublicIPv6 string `json:"public_ipv6"`
PublicIPv6Interface string `json:"public_ipv6_interface"`
IPv6Prefixes []lxc.IPv6PrefixInfo `json:"ipv6_prefixes"`
}
type DiskIOInfo struct {
ReadBytes uint64 `json:"read_bytes"`
WriteBytes uint64 `json:"write_bytes"`
ReadBps float64 `json:"read_bps"`
WriteBps float64 `json:"write_bps"`
}
var hostCPUMu sync.Mutex
var lastHostCPU cpuTimes
var hostIOMu sync.Mutex
var lastHostIO hostIOSample
type cpuTimes struct {
Total uint64
Idle uint64
}
type hostIOSample struct {
RXBytes uint64
TXBytes uint64
ReadBytes uint64
WriteBytes uint64
At int64
}
func getHostInfo() HostInfo {
info := HostInfo{
CPU: CpuInfo{Cores: runtime.NumCPU()},
}
info.RAM = getMemoryInfo()
info.Disk = getDiskInfo()
info.CPU.Usage = getCPUUsage()
info.Network, info.DiskIO = getHostRates()
info.Load = getLoadInfo()
return info
}
func getMemoryInfo() MemoryInfo {
f, err := os.Open("/proc/meminfo")
if err != nil {
return MemoryInfo{TotalMB: 0, UsedMB: 0, FreeMB: 0}
}
defer f.Close()
var total, available, free int64
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
val, _ := strconv.ParseInt(fields[1], 10, 64)
switch fields[0] {
case "MemTotal:":
total = val / 1024
case "MemAvailable:":
available = val / 1024
case "MemFree:":
free = val / 1024
}
}
used := total - available
if available == 0 {
used = total - free
}
return MemoryInfo{
TotalMB: total,
UsedMB: used,
FreeMB: available,
}
}
func getDiskInfo() DiskInfo {
var stat syscall.Statfs_t
if err := syscall.Statfs("/", &stat); err != nil {
// Try command-based fallback
cmd := exec.Command("df", "-BG", "/")
output, err := cmd.Output()
if err == nil {
lines := strings.Split(string(output), "\n")
if len(lines) >= 2 {
fields := strings.Fields(lines[1])
if len(fields) >= 4 {
total, _ := parseSizeGBf(fields[1])
used, _ := parseSizeGBf(fields[2])
free, _ := parseSizeGBf(fields[3])
return DiskInfo{TotalGB: total, UsedGB: used, FreeGB: free}
}
}
}
return DiskInfo{}
}
total := float64(int64(stat.Blocks)*int64(stat.Bsize)) / (1024 * 1024 * 1024)
free := float64(int64(stat.Bavail)*int64(stat.Bsize)) / (1024 * 1024 * 1024)
used := total - free
return DiskInfo{
TotalGB: total,
UsedGB: used,
FreeGB: free,
}
}
func getCPUUsage() float64 {
current, err := readCPUTimes()
if err != nil {
return 0
}
hostCPUMu.Lock()
defer hostCPUMu.Unlock()
if lastHostCPU.Total == 0 {
lastHostCPU = current
return 0
}
totalDelta := current.Total - lastHostCPU.Total
idleDelta := current.Idle - lastHostCPU.Idle
lastHostCPU = current
if totalDelta == 0 {
return 0
}
usage := (1 - float64(idleDelta)/float64(totalDelta)) * 100
if usage < 0 {
return 0
}
if usage > 100 {
return 100
}
return usage
}
func readCPUTimes() (cpuTimes, error) {
f, err := os.Open("/proc/stat")
if err != nil {
return cpuTimes{}, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
if !scanner.Scan() {
return cpuTimes{}, scanner.Err()
}
fields := strings.Fields(scanner.Text())
if len(fields) < 8 || fields[0] != "cpu" {
return cpuTimes{}, nil
}
var values []uint64
for _, field := range fields[1:] {
value, _ := strconv.ParseUint(field, 10, 64)
values = append(values, value)
}
var total uint64
for _, value := range values {
total += value
}
idle := values[3]
if len(values) > 4 {
idle += values[4]
}
return cpuTimes{Total: total, Idle: idle}, nil
}
func parseSizeGB(s string) (int64, error) {
s = strings.TrimSuffix(s, "G")
s = strings.TrimSpace(s)
val, err := strconv.ParseInt(s, 10, 64)
return val, err
}
func parseSizeGBf(s string) (float64, error) {
s = strings.TrimSuffix(s, "G")
s = strings.TrimSpace(s)
val, err := strconv.ParseFloat(s, 64)
return val, err
}
func getHostRates() (NetworkInfo, DiskIOInfo) {
rx, tx := readHostNetworkBytes()
readBytes, writeBytes := readHostDiskBytes()
now := unixNano()
network := NetworkInfo{RXBytes: rx, TXBytes: tx}
publicIPv4 := lxc.DetectPublicIPv4()
network.PublicIPv4 = publicIPv4.Address
network.PublicIPv4Interface = publicIPv4.Interface
network.IPv6Prefixes = lxc.DetectPublicIPv6Prefixes()
if len(network.IPv6Prefixes) > 0 {
network.PublicIPv6 = network.IPv6Prefixes[0].Address
network.PublicIPv6Interface = network.IPv6Prefixes[0].Interface
}
diskIO := DiskIOInfo{ReadBytes: readBytes, WriteBytes: writeBytes}
hostIOMu.Lock()
defer hostIOMu.Unlock()
if lastHostIO.At == 0 {
lastHostIO = hostIOSample{RXBytes: rx, TXBytes: tx, ReadBytes: readBytes, WriteBytes: writeBytes, At: now}
return network, diskIO
}
elapsed := float64(now-lastHostIO.At) / 1_000_000_000
if elapsed > 0 {
if rx >= lastHostIO.RXBytes {
network.RXBps = float64(rx-lastHostIO.RXBytes) / elapsed
}
if tx >= lastHostIO.TXBytes {
network.TXBps = float64(tx-lastHostIO.TXBytes) / elapsed
}
if readBytes >= lastHostIO.ReadBytes {
diskIO.ReadBps = float64(readBytes-lastHostIO.ReadBytes) / elapsed
}
if writeBytes >= lastHostIO.WriteBytes {
diskIO.WriteBps = float64(writeBytes-lastHostIO.WriteBytes) / elapsed
}
}
lastHostIO = hostIOSample{RXBytes: rx, TXBytes: tx, ReadBytes: readBytes, WriteBytes: writeBytes, At: now}
return network, diskIO
}
func readHostNetworkBytes() (uint64, uint64) {
entries, err := os.ReadDir("/sys/class/net")
if err != nil {
return 0, 0
}
var rx, tx uint64
for _, entry := range entries {
name := entry.Name()
if name == "lo" {
continue
}
rx += readUintFile("/sys/class/net/" + name + "/statistics/rx_bytes")
tx += readUintFile("/sys/class/net/" + name + "/statistics/tx_bytes")
}
return rx, tx
}
func readHostDiskBytes() (uint64, uint64) {
f, err := os.Open("/proc/diskstats")
if err != nil {
return 0, 0
}
defer f.Close()
var readSectors, writeSectors uint64
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 14 {
continue
}
device := fields[2]
if strings.HasPrefix(device, "loop") ||
strings.HasPrefix(device, "ram") ||
strings.HasPrefix(device, "fd") ||
strings.HasPrefix(device, "sr") {
continue
}
read, _ := strconv.ParseUint(fields[5], 10, 64)
write, _ := strconv.ParseUint(fields[9], 10, 64)
readSectors += read
writeSectors += write
}
return readSectors * 512, writeSectors * 512
}
func readUintFile(path string) uint64 {
data, err := os.ReadFile(path)
if err != nil {
return 0
}
value, _ := strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
return value
}
func unixNano() int64 {
return time.Now().UnixNano()
}
func getLoadInfo() LoadInfo {
f, err := os.Open("/proc/loadavg")
if err != nil {
return LoadInfo{}
}
defer f.Close()
scanner := bufio.NewScanner(f)
if !scanner.Scan() {
return LoadInfo{}
}
fields := strings.Fields(scanner.Text())
if len(fields) < 3 {
return LoadInfo{}
}
load1, _ := strconv.ParseFloat(fields[0], 64)
load5, _ := strconv.ParseFloat(fields[1], 64)
load15, _ := strconv.ParseFloat(fields[2], 64)
return LoadInfo{Load1: load1, Load5: load5, Load15: load15}
}