160 lines
4.1 KiB
Go
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
|
|
}
|