Files
ProxmoxDash/activity.go

190 lines
5.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"sort"
"strconv"
"strings"
"sync"
"time"
)
type ProxmoxTask struct {
UPID string `json:"upid"`
Node string `json:"node"`
Type string `json:"type"`
ID string `json:"id"`
User string `json:"user"`
Status string `json:"status"`
StartTime int64 `json:"starttime"`
EndTime int64 `json:"endtime"`
Duration int64 `json:"duration"`
}
type GuestSnapshot struct {
VMID int `json:"vmid"`
GuestName string `json:"guestName"`
GuestType string `json:"guestType"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt int64 `json:"createdAt"`
Running bool `json:"running"`
Parent string `json:"parent,omitempty"`
SizeBytes *uint64 `json:"sizeBytes,omitempty"`
AgeSeconds int64 `json:"ageSeconds"`
}
type ProxmoxActivity struct {
Available bool `json:"available"`
Tasks []ProxmoxTask `json:"tasks"`
Snapshots []GuestSnapshot `json:"snapshots"`
CheckedAt int64 `json:"checkedAt"`
}
type activityCollector struct {
mu sync.RWMutex
value ProxmoxActivity
refreshing bool
}
func newActivityCollector() *activityCollector {
c := &activityCollector{refreshing: true}
go c.refresh()
return c
}
func (c *activityCollector) collect(guests GuestsMetrics) ProxmoxActivity {
c.mu.RLock()
value := c.value
c.mu.RUnlock()
c.mu.Lock()
if !c.refreshing && time.Since(time.Unix(value.CheckedAt, 0)) > 15*time.Second {
c.refreshing = true
go c.refreshWithGuests(guests)
}
c.mu.Unlock()
return value
}
func (c *activityCollector) refresh() { c.refreshWithGuests(readGuests()) }
func (c *activityCollector) refreshWithGuests(guests GuestsMetrics) {
value := readProxmoxActivity(guests)
c.mu.Lock()
c.value = value
c.refreshing = false
c.mu.Unlock()
}
func readProxmoxActivity(guests GuestsMetrics) ProxmoxActivity {
result := ProxmoxActivity{CheckedAt: time.Now().Unix()}
if _, err := exec.LookPath("pvesh"); err != nil {
return result
}
node, err := os.Hostname()
if err != nil {
return result
}
result.Available = true
out := commandOutput(8*time.Second, "pvesh", "get", "/nodes/"+node+"/tasks", "--limit", "100", "--output-format", "json")
if out != "" && json.Unmarshal([]byte(out), &result.Tasks) == nil {
for i := range result.Tasks {
if result.Tasks[i].EndTime > result.Tasks[i].StartTime {
result.Tasks[i].Duration = result.Tasks[i].EndTime - result.Tasks[i].StartTime
}
}
}
for _, guest := range guests.Guests {
if guest.Template != 0 {
continue
}
kind := "qemu"
label := "VM"
if guest.Type != "qemu" {
kind = "lxc"
label = "LXC"
}
out := commandOutput(5*time.Second, "pvesh", "get", fmt.Sprintf("/nodes/%s/%s/%d/snapshot", guest.Node, kind, guest.VMID), "--output-format", "json")
if out == "" {
continue
}
var raw []struct {
Name string `json:"name"`
Description string `json:"description"`
SnapTime int64 `json:"snaptime"`
Running int `json:"running"`
Parent string `json:"parent"`
VMState any `json:"vmstate"`
}
if json.Unmarshal([]byte(out), &raw) != nil {
continue
}
for _, s := range raw {
if s.Name == "current" {
continue
}
item := GuestSnapshot{VMID: guest.VMID, GuestName: guest.Name, GuestType: label, Name: s.Name, Description: s.Description, CreatedAt: s.SnapTime, Running: s.Running != 0, Parent: s.Parent}
if item.CreatedAt > 0 {
item.AgeSeconds = time.Now().Unix() - item.CreatedAt
}
if size := uintValue(s.VMState); size > 1 {
item.SizeBytes = &size
}
result.Snapshots = append(result.Snapshots, item)
}
}
sort.Slice(result.Snapshots, func(i, j int) bool { return result.Snapshots[i].CreatedAt > result.Snapshots[j].CreatedAt })
return result
}
func uintValue(v any) uint64 {
switch n := v.(type) {
case float64:
return uint64(n)
case string:
value, _ := strconv.ParseUint(n, 10, 64)
return value
}
return 0
}
type BackupCheckResult struct {
OK bool `json:"ok"`
Level string `json:"level"`
Message string `json:"message"`
CheckedAt int64 `json:"checkedAt"`
}
func verifyBackup(volid string) BackupCheckResult {
result := BackupCheckResult{CheckedAt: time.Now().Unix(), Level: "metadata"}
if volid == "" || strings.ContainsAny(volid, "\r\n") {
result.Message = "Некорректный идентификатор backup"
return result
}
out, err := exec.Command("pvesm", "path", volid).CombinedOutput()
if err != nil {
result.Message = "Backup не найден в подключённом storage: " + strings.TrimSpace(string(out))
return result
}
path := strings.TrimSpace(string(out))
if strings.HasPrefix(path, "pbs://") {
ctxOutput := commandOutput(20*time.Second, "pvesm", "extractconfig", volid)
if ctxOutput == "" {
result.Message = "Snapshot найден в PBS, но конфигурацию для тестового чтения получить не удалось"
return result
}
result.OK = true
result.Level = "pbs-metadata"
result.Message = "PBS snapshot и его конфигурация успешно прочитаны. Полное восстановление VM не запускалось."
return result
}
info, err := os.Stat(path)
if err != nil {
result.Message = "Метаданные доступны, но локальный файл не открыт: " + err.Error()
return result
}
if info.Size() == 0 {
result.Message = "Файл backup пуст"
return result
}
result.OK = true
result.Level = "readability"
result.Message = fmt.Sprintf("Файл доступен для чтения, размер %d байт. Полное тестовое восстановление не запускалось.", info.Size())
return result
}