Initial Proxmox dashboard with host and guest metrics
This commit is contained in:
102
zfs.go
Normal file
102
zfs.go
Normal file
@@ -0,0 +1,102 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user