117 lines
2.9 KiB
Go
117 lines
2.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"os/exec"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type BackupItem struct {
|
|
VMID int `json:"vmid"`
|
|
Storage string `json:"storage"`
|
|
VolumeID string `json:"volid"`
|
|
Format string `json:"format"`
|
|
SizeBytes uint64 `json:"size"`
|
|
CreatedAt int64 `json:"ctime"`
|
|
}
|
|
|
|
type BackupMetrics struct {
|
|
Available bool `json:"available"`
|
|
Items []BackupItem `json:"items"`
|
|
}
|
|
|
|
type backupCollector struct {
|
|
mu sync.Mutex
|
|
cached BackupMetrics
|
|
updatedAt time.Time
|
|
}
|
|
|
|
func (c *backupCollector) collect() BackupMetrics {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if time.Since(c.updatedAt) < time.Minute {
|
|
return c.cached
|
|
}
|
|
c.cached = readBackups()
|
|
c.updatedAt = time.Now()
|
|
return c.cached
|
|
}
|
|
|
|
func readBackups() BackupMetrics {
|
|
if _, err := exec.LookPath("pvesh"); err != nil {
|
|
return BackupMetrics{}
|
|
}
|
|
node, err := os.Hostname()
|
|
if err != nil {
|
|
return BackupMetrics{Available: true}
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
|
defer cancel()
|
|
output, err := exec.CommandContext(ctx, "pvesh", "get", "/nodes/"+node+"/storage", "--content", "backup", "--enabled", "1", "--output-format", "json").Output()
|
|
result := BackupMetrics{Available: true}
|
|
if err != nil {
|
|
return result
|
|
}
|
|
var storages []struct {
|
|
Storage string `json:"storage"`
|
|
Active int `json:"active"`
|
|
}
|
|
if json.Unmarshal(output, &storages) != nil {
|
|
return result
|
|
}
|
|
var items []BackupItem
|
|
for _, storage := range storages {
|
|
if storage.Storage == "" || storage.Active == 0 {
|
|
continue
|
|
}
|
|
content, err := exec.CommandContext(ctx, "pvesh", "get", "/nodes/"+node+"/storage/"+storage.Storage+"/content", "--content", "backup", "--output-format", "json").Output()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
var storageItems []BackupItem
|
|
if json.Unmarshal(content, &storageItems) == nil {
|
|
for i := range storageItems {
|
|
storageItems[i].Storage = storage.Storage
|
|
if storageItems[i].Format == "" {
|
|
storageItems[i].Format = backupFormat(storageItems[i].VolumeID)
|
|
}
|
|
}
|
|
items = append(items, storageItems...)
|
|
}
|
|
}
|
|
result.Items = latestBackups(items)
|
|
return result
|
|
}
|
|
|
|
func latestBackups(items []BackupItem) []BackupItem {
|
|
latest := make(map[int]BackupItem)
|
|
for _, item := range items {
|
|
if item.VMID == 0 {
|
|
continue
|
|
}
|
|
if previous, ok := latest[item.VMID]; !ok || item.CreatedAt > previous.CreatedAt {
|
|
latest[item.VMID] = item
|
|
}
|
|
}
|
|
result := make([]BackupItem, 0, len(latest))
|
|
for _, item := range latest {
|
|
result = append(result, item)
|
|
}
|
|
sort.Slice(result, func(i, j int) bool { return result[i].VMID < result[j].VMID })
|
|
return result
|
|
}
|
|
|
|
func backupFormat(volumeID string) string {
|
|
for _, suffix := range []string{".vma.zst", ".vma.lzo", ".vma.gz", ".tar.zst", ".tar.lzo", ".tar.gz"} {
|
|
if strings.HasSuffix(volumeID, suffix) {
|
|
return strings.TrimPrefix(suffix, ".")
|
|
}
|
|
}
|
|
return "backup"
|
|
}
|