Files
ProxmoxDash/zfs.go

103 lines
3.1 KiB
Go

package main
import (
"context"
"os/exec"
"strconv"
"strings"
"sync"
"time"
)
type ZFSPoolMetrics struct {
Name string `json:"name"`
Health string `json:"health"`
SizeBytes uint64 `json:"sizeBytes"`
AllocatedBytes uint64 `json:"allocatedBytes"`
FreeBytes uint64 `json:"freeBytes"`
CapacityPercent float64 `json:"capacityPercent"`
Fragmentation float64 `json:"fragmentationPercent"`
DedupRatio string `json:"dedupRatio"`
Scan string `json:"scan"`
Errors string `json:"errors"`
AttentionReasons []string `json:"attentionReasons"`
}
type ZFSMetrics struct {
Available bool `json:"available"`
Pools []ZFSPoolMetrics `json:"pools"`
}
type zfsCollector struct {
mu sync.Mutex
cached ZFSMetrics
updatedAt time.Time
}
func (c *zfsCollector) collect() ZFSMetrics {
c.mu.Lock()
defer c.mu.Unlock()
if time.Since(c.updatedAt) < 30*time.Second {
return c.cached
}
c.cached = readZFSPools()
c.updatedAt = time.Now()
return c.cached
}
func readZFSPools() ZFSMetrics {
if _, err := exec.LookPath("zpool"); err != nil {
return ZFSMetrics{}
}
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
defer cancel()
output, err := exec.CommandContext(ctx, "zpool", "list", "-H", "-p", "-o", "name,size,allocated,free,fragmentation,capacity,dedupratio,health").Output()
result := ZFSMetrics{Available: true}
if err != nil {
return result
}
for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") {
fields := strings.Fields(line)
if len(fields) < 8 {
continue
}
pool := ZFSPoolMetrics{Name: fields[0], DedupRatio: fields[6], Health: fields[7]}
pool.SizeBytes, _ = strconv.ParseUint(fields[1], 10, 64)
pool.AllocatedBytes, _ = strconv.ParseUint(fields[2], 10, 64)
pool.FreeBytes, _ = strconv.ParseUint(fields[3], 10, 64)
pool.Fragmentation = parsePercent(fields[4])
pool.CapacityPercent = parsePercent(fields[5])
pool.Scan, pool.Errors = readZFSStatus(pool.Name)
if pool.Health != "ONLINE" {
pool.AttentionReasons = append(pool.AttentionReasons, "Состояние пула: "+pool.Health)
}
if pool.Errors != "" && !strings.Contains(pool.Errors, "No known data errors") {
pool.AttentionReasons = append(pool.AttentionReasons, pool.Errors)
}
result.Pools = append(result.Pools, pool)
}
return result
}
func parsePercent(value string) float64 {
parsed, _ := strconv.ParseFloat(strings.TrimSuffix(value, "%"), 64)
return parsed
}
func readZFSStatus(pool string) (string, string) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
output, _ := exec.CommandContext(ctx, "zpool", "status", pool).Output()
var scan, errors string
for _, line := range strings.Split(string(output), "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "scan:") {
scan = strings.TrimSpace(strings.TrimPrefix(trimmed, "scan:"))
}
if strings.HasPrefix(trimmed, "errors:") {
errors = strings.TrimSpace(strings.TrimPrefix(trimmed, "errors:"))
}
}
return scan, errors
}