mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-06-11 17:10:28 +00:00
237 lines
5.3 KiB
Go
237 lines
5.3 KiB
Go
package services
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/mailcow/mailcow-dockerized/agent/internal/commands"
|
|
"github.com/mailcow/mailcow-dockerized/agent/internal/proc"
|
|
)
|
|
|
|
func init() { Register("host", buildHost) }
|
|
|
|
// hostProcRoot is where the host-agent container mounts /proc. If we're not
|
|
// running as host-agent, falling back to /proc still produces sensible numbers
|
|
// (the container's own view) so dashboards don't blank out in unit tests.
|
|
var hostProcRoot = "/host/proc"
|
|
|
|
func resolveProc(p string) string {
|
|
if _, err := os.Stat(hostProcRoot); err == nil {
|
|
return hostProcRoot + p
|
|
}
|
|
return "/proc" + p
|
|
}
|
|
|
|
func buildHost(_ *proc.Supervisor) *commands.Table {
|
|
t := commands.New("host")
|
|
// No lifecycle — the host-agent container has no main process to manage.
|
|
|
|
t.Register("exec.df", func(ctx context.Context, args map[string]any) (any, error) {
|
|
path := commands.ArgStringOpt(args, "path", "/")
|
|
var stat syscall.Statfs_t
|
|
if err := syscall.Statfs(path, &stat); err != nil {
|
|
return nil, fmt.Errorf("statfs %s: %w", path, err)
|
|
}
|
|
size := int64(stat.Blocks) * int64(stat.Bsize)
|
|
free := int64(stat.Bavail) * int64(stat.Bsize)
|
|
used := size - free
|
|
return map[string]any{
|
|
"path": path,
|
|
"size": size,
|
|
"used": used,
|
|
"available": free,
|
|
}, nil
|
|
})
|
|
|
|
t.Register("exec.host-stats", func(ctx context.Context, _ map[string]any) (any, error) {
|
|
return readHostStats()
|
|
})
|
|
|
|
return t
|
|
}
|
|
|
|
func readHostStats() (map[string]any, error) {
|
|
out := map[string]any{
|
|
"system_time": time.Now().Format("2006-01-02 15:04:05"),
|
|
"architecture": readArchitecture(),
|
|
}
|
|
|
|
if uptime, err := readUptime(); err == nil {
|
|
out["uptime"] = int64(uptime)
|
|
} else {
|
|
out["uptime"] = int64(0)
|
|
}
|
|
|
|
cores := readCPUCores()
|
|
cpuUsage, _ := sampleHostCPU(500 * time.Millisecond)
|
|
out["cpu"] = map[string]any{
|
|
"cores": cores,
|
|
"usage": cpuUsage,
|
|
}
|
|
|
|
memTotal, memUsage := readMemoryTotalAndUsagePct()
|
|
out["memory"] = map[string]any{
|
|
"total": memTotal, // bytes
|
|
"usage": memUsage, // percent 0..100
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// readArchitecture returns the host's machine architecture (e.g. "x86_64",
|
|
// "aarch64"). Falls back to a single dash if syscall.Uname fails.
|
|
func readArchitecture() string {
|
|
var u syscall.Utsname
|
|
if err := syscall.Uname(&u); err != nil {
|
|
return "-"
|
|
}
|
|
return charsToString(u.Machine[:])
|
|
}
|
|
|
|
func charsToString(b []int8) string {
|
|
out := make([]byte, 0, len(b))
|
|
for _, c := range b {
|
|
if c == 0 {
|
|
break
|
|
}
|
|
out = append(out, byte(c))
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
// readCPUCores counts `^processor` lines in /proc/cpuinfo. On a container
|
|
// with /host/proc bind-mounted this gives the host's logical CPU count,
|
|
// not the container's cgroup limits.
|
|
func readCPUCores() int {
|
|
f, err := os.Open(resolveProc("/cpuinfo"))
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
defer f.Close()
|
|
n := 0
|
|
sc := bufio.NewScanner(f)
|
|
for sc.Scan() {
|
|
if strings.HasPrefix(sc.Text(), "processor") {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
// readMemoryTotalAndUsagePct reads /proc/meminfo and returns (total_bytes,
|
|
// usage_pct_0_100). "Usage" is computed as (Total - Available)/Total which
|
|
// matches what tools like `free` show as "used".
|
|
func readMemoryTotalAndUsagePct() (int64, int) {
|
|
f, err := os.Open(resolveProc("/meminfo"))
|
|
if err != nil {
|
|
return 0, 0
|
|
}
|
|
defer f.Close()
|
|
|
|
var total, available int64
|
|
sc := bufio.NewScanner(f)
|
|
for sc.Scan() {
|
|
fields := strings.Fields(sc.Text())
|
|
if len(fields) < 2 {
|
|
continue
|
|
}
|
|
switch fields[0] {
|
|
case "MemTotal:":
|
|
total = parseInt64(fields[1]) * 1024
|
|
case "MemAvailable:":
|
|
available = parseInt64(fields[1]) * 1024
|
|
}
|
|
}
|
|
if total <= 0 {
|
|
return 0, 0
|
|
}
|
|
used := total - available
|
|
if available <= 0 {
|
|
used = total
|
|
}
|
|
pct := int(float64(used) / float64(total) * 100.0)
|
|
if pct < 0 {
|
|
pct = 0
|
|
}
|
|
if pct > 100 {
|
|
pct = 100
|
|
}
|
|
return total, pct
|
|
}
|
|
|
|
func readUptime() (float64, error) {
|
|
b, err := os.ReadFile(resolveProc("/uptime"))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
fields := strings.Fields(string(b))
|
|
if len(fields) < 1 {
|
|
return 0, fmt.Errorf("malformed uptime")
|
|
}
|
|
return strconv.ParseFloat(fields[0], 64)
|
|
}
|
|
|
|
// sampleHostCPU returns CPU utilization (0..100) sampled over `window`.
|
|
func sampleHostCPU(window time.Duration) (float64, error) {
|
|
a, err := readCPULine()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
time.Sleep(window)
|
|
b, err := readCPULine()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
totalA, totalB := sum(a), sum(b)
|
|
idleA, idleB := a[3], b[3]
|
|
dTotal, dIdle := totalB-totalA, idleB-idleA
|
|
if dTotal == 0 {
|
|
return 0, nil
|
|
}
|
|
return 100.0 * float64(dTotal-dIdle) / float64(dTotal), nil
|
|
}
|
|
|
|
func readCPULine() ([]int64, error) {
|
|
f, err := os.Open(resolveProc("/stat"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
sc := bufio.NewScanner(f)
|
|
if !sc.Scan() {
|
|
return nil, fmt.Errorf("empty /proc/stat")
|
|
}
|
|
fields := strings.Fields(sc.Text())
|
|
if len(fields) < 5 || fields[0] != "cpu" {
|
|
return nil, fmt.Errorf("unexpected /proc/stat first line")
|
|
}
|
|
out := make([]int64, 0, len(fields)-1)
|
|
for _, f := range fields[1:] {
|
|
n, err := strconv.ParseInt(f, 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, n)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func sum(xs []int64) int64 {
|
|
var s int64
|
|
for _, x := range xs {
|
|
s += x
|
|
}
|
|
return s
|
|
}
|
|
|
|
func parseInt64(s string) int64 {
|
|
n, _ := strconv.ParseInt(s, 10, 64)
|
|
return n
|
|
}
|