Files
2026-06-16 21:52:10 +04:00

160 lines
4.1 KiB
Go

package cluster
import (
"math"
"sort"
"strings"
"time"
"pxmon/internal/history"
)
type AvailabilityVM struct {
Name string `json:"name"`
Availability float64 `json:"availability_pct"`
Samples int `json:"samples"`
Running int `json:"running_samples"`
}
type AvailabilityReport struct {
Cluster string `json:"cluster"`
Range string `json:"range"`
Samples int `json:"samples"`
UpSamples int `json:"up_samples"`
Availability float64 `json:"availability_pct"`
VMs []AvailabilityVM `json:"vms,omitempty"`
}
func (s *Service) AvailabilityReport(selector string, since time.Time, vmFilter string) (AvailabilityReport, error) {
c, err := s.Get(selector)
if err != nil {
return AvailabilityReport{}, err
}
store := history.NewAvailabilityStore(s.DataDir())
snaps, err := store.Load(c.ID, since)
if err != nil {
return AvailabilityReport{}, err
}
rep := AvailabilityReport{Cluster: c.Name, Samples: len(snaps)}
if len(snaps) == 0 {
return rep, nil
}
vmFilter = strings.TrimSpace(vmFilter)
totalUp := 0
type acc struct{ samples, running int }
vmap := map[string]*acc{}
for _, snap := range snaps {
if snap.ClusterUp {
totalUp++
}
for vm, st := range snap.VMStates {
if vmFilter != "" && !strings.EqualFold(vmFilter, vm) {
continue
}
a := vmap[vm]
if a == nil {
a = &acc{}
vmap[vm] = a
}
a.samples++
if strings.EqualFold(strings.TrimSpace(st), "running") {
a.running++
}
}
}
rep.UpSamples = totalUp
rep.Availability = 100 * float64(totalUp) / float64(len(snaps))
for vm, a := range vmap {
if a.samples == 0 {
continue
}
rep.VMs = append(rep.VMs, AvailabilityVM{
Name: vm,
Samples: a.samples,
Running: a.running,
Availability: 100 * float64(a.running) / float64(a.samples),
})
}
sort.Slice(rep.VMs, func(i, j int) bool { return rep.VMs[i].Name < rep.VMs[j].Name })
return rep, nil
}
type CapacityForecastItem struct {
Mount string `json:"mount"`
UsedPct float64 `json:"used_pct"`
SlopeBytesSec float64 `json:"slope_bytes_per_sec"`
DaysTo90 float64 `json:"days_to_90_pct"`
DaysTo95 float64 `json:"days_to_95_pct"`
}
type CapacityForecastReport struct {
Cluster string `json:"cluster"`
Samples int `json:"samples"`
Items []CapacityForecastItem `json:"items,omitempty"`
}
func (s *Service) CapacityForecast(selector string, since time.Time) (CapacityForecastReport, error) {
c, err := s.Get(selector)
if err != nil {
return CapacityForecastReport{}, err
}
store := history.NewCapacityStore(s.DataDir())
snaps, err := store.Load(c.ID, since)
if err != nil {
return CapacityForecastReport{}, err
}
rep := CapacityForecastReport{Cluster: c.Name, Samples: len(snaps)}
if len(snaps) < 2 {
return rep, nil
}
type point struct {
ts time.Time
used float64
tot float64
}
byMount := map[string][]point{}
for _, snap := range snaps {
for _, d := range snap.Disks {
if d.TotalBytes == 0 {
continue
}
byMount[d.Mount] = append(byMount[d.Mount], point{ts: snap.Timestamp, used: float64(d.UsedBytes), tot: float64(d.TotalBytes)})
}
}
for mnt, pts := range byMount {
if len(pts) < 2 {
continue
}
sort.Slice(pts, func(i, j int) bool { return pts[i].ts.Before(pts[j].ts) })
first := pts[0]
last := pts[len(pts)-1]
dt := last.ts.Sub(first.ts).Seconds()
if dt <= 0 {
continue
}
slope := (last.used - first.used) / dt
usedPct := 100 * last.used / last.tot
d90 := daysToTarget(last.used, last.tot*0.90, slope)
d95 := daysToTarget(last.used, last.tot*0.95, slope)
rep.Items = append(rep.Items, CapacityForecastItem{
Mount: mnt,
UsedPct: usedPct,
SlopeBytesSec: slope,
DaysTo90: d90,
DaysTo95: d95,
})
}
sort.Slice(rep.Items, func(i, j int) bool { return rep.Items[i].UsedPct > rep.Items[j].UsedPct })
return rep, nil
}
func daysToTarget(current, target, slope float64) float64 {
if target <= current {
return 0
}
if slope <= 0 {
return math.Inf(1)
}
return (target - current) / slope / 86400
}