Files
ProxmoxDash/backups.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"
}