128 lines
3.5 KiB
Go
128 lines
3.5 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"`
|
|
PreviousSizeBytes uint64 `json:"previousSize,omitempty"`
|
|
SizeChangePercent *float64 `json:"sizeChangePercent,omitempty"`
|
|
PreviousCreatedAt int64 `json:"previousCtime,omitempty"`
|
|
}
|
|
|
|
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 {
|
|
byGuest := make(map[int][]BackupItem)
|
|
for _, item := range items {
|
|
if item.VMID == 0 {
|
|
continue
|
|
}
|
|
byGuest[item.VMID] = append(byGuest[item.VMID], item)
|
|
}
|
|
result := make([]BackupItem, 0, len(byGuest))
|
|
for _, history := range byGuest {
|
|
sort.Slice(history, func(i, j int) bool { return history[i].CreatedAt > history[j].CreatedAt })
|
|
item := history[0]
|
|
if len(history) > 1 {
|
|
item.PreviousSizeBytes = history[1].SizeBytes
|
|
item.PreviousCreatedAt = history[1].CreatedAt
|
|
if history[1].SizeBytes > 0 {
|
|
change := (float64(item.SizeBytes) - float64(history[1].SizeBytes)) / float64(history[1].SizeBytes) * 100
|
|
item.SizeChangePercent = &change
|
|
}
|
|
}
|
|
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"
|
|
}
|