All checks were successful
continuous-integration/drone/push Build is passing
232 lines
6.8 KiB
Go
232 lines
6.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"vctp/components/views"
|
|
"vctp/db"
|
|
)
|
|
|
|
// VcenterList renders a list of vCenters being monitored.
|
|
// @Summary List vCenters
|
|
// @Description Lists all vCenters with recorded snapshot totals.
|
|
// @Tags vcenters
|
|
// @Produce text/html
|
|
// @Success 200 {string} string "HTML page"
|
|
// @Router /vcenters [get]
|
|
func (h *Handler) VcenterList(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
if err := db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB()); err != nil {
|
|
h.Logger.Warn("failed to sync vcenter totals", "error", err)
|
|
}
|
|
vcs, err := db.ListVcenters(ctx, h.Database.DB())
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("failed to list vcenters: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
links := make([]views.VcenterLink, 0, len(vcs))
|
|
for _, vc := range vcs {
|
|
links = append(links, views.VcenterLink{
|
|
Name: vc,
|
|
Link: "/vcenters/totals?vcenter=" + url.QueryEscape(vc),
|
|
})
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if err := views.VcenterList(links).Render(ctx, w); err != nil {
|
|
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// VcenterTotals renders totals for a vCenter.
|
|
// @Summary vCenter totals
|
|
// @Description Shows per-snapshot totals for a vCenter.
|
|
// @Tags vcenters
|
|
// @Produce text/html
|
|
// @Param vcenter query string true "vCenter URL"
|
|
// @Param type query string false "hourly|daily|monthly (default: hourly)"
|
|
// @Param limit query int false "Limit results (default 200)"
|
|
// @Success 200 {string} string "HTML page"
|
|
// @Failure 400 {string} string "Missing vcenter"
|
|
// @Router /vcenters/totals [get]
|
|
func (h *Handler) VcenterTotals(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
vc := r.URL.Query().Get("vcenter")
|
|
if vc == "" {
|
|
http.Error(w, "vcenter is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
viewType := strings.ToLower(r.URL.Query().Get("type"))
|
|
if viewType == "" {
|
|
viewType = "hourly"
|
|
}
|
|
switch viewType {
|
|
case "hourly", "daily", "monthly":
|
|
default:
|
|
viewType = "hourly"
|
|
}
|
|
if viewType == "hourly" {
|
|
if err := db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB()); err != nil {
|
|
h.Logger.Warn("failed to sync vcenter totals", "error", err)
|
|
}
|
|
}
|
|
limit := 200
|
|
if l := r.URL.Query().Get("limit"); l != "" {
|
|
if v, err := strconv.Atoi(l); err == nil && v > 0 {
|
|
limit = v
|
|
}
|
|
}
|
|
rows, err := db.ListVcenterTotalsByType(ctx, h.Database.DB(), vc, viewType, limit)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("failed to list totals: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
entries := make([]views.VcenterTotalsEntry, 0, len(rows))
|
|
for _, row := range rows {
|
|
entries = append(entries, views.VcenterTotalsEntry{
|
|
Snapshot: time.Unix(row.SnapshotTime, 0).Local().Format("2006-01-02 15:04:05"),
|
|
RawTime: row.SnapshotTime,
|
|
VmCount: row.VmCount,
|
|
VcpuTotal: row.VcpuTotal,
|
|
RamTotalGB: row.RamTotalGB,
|
|
})
|
|
}
|
|
chart := buildVcenterChart(entries)
|
|
meta := buildVcenterMeta(vc, viewType)
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if err := views.VcenterTotalsPage(vc, entries, chart, meta).Render(ctx, w); err != nil {
|
|
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func buildVcenterMeta(vcenter string, viewType string) views.VcenterTotalsMeta {
|
|
active := viewType
|
|
if active == "" {
|
|
active = "hourly"
|
|
}
|
|
meta := views.VcenterTotalsMeta{
|
|
ViewType: active,
|
|
TypeLabel: "Hourly",
|
|
HourlyLink: "/vcenters/totals?vcenter=" + url.QueryEscape(vcenter) + "&type=hourly",
|
|
DailyLink: "/vcenters/totals?vcenter=" + url.QueryEscape(vcenter) + "&type=daily",
|
|
MonthlyLink: "/vcenters/totals?vcenter=" + url.QueryEscape(vcenter) + "&type=monthly",
|
|
HourlyClass: "web3-button",
|
|
DailyClass: "web3-button",
|
|
MonthlyClass: "web3-button",
|
|
}
|
|
switch active {
|
|
case "daily":
|
|
meta.TypeLabel = "Daily"
|
|
meta.DailyClass = "web3-button active"
|
|
case "monthly":
|
|
meta.TypeLabel = "Monthly"
|
|
meta.MonthlyClass = "web3-button active"
|
|
default:
|
|
meta.ViewType = "hourly"
|
|
meta.HourlyClass = "web3-button active"
|
|
}
|
|
return meta
|
|
}
|
|
|
|
func buildVcenterChart(entries []views.VcenterTotalsEntry) views.VcenterChartData {
|
|
if len(entries) == 0 {
|
|
return views.VcenterChartData{}
|
|
}
|
|
// Plot oldest on the left, newest on the right.
|
|
plot := make([]views.VcenterTotalsEntry, 0, len(entries))
|
|
for i := len(entries) - 1; i >= 0; i-- {
|
|
plot = append(plot, entries[i])
|
|
}
|
|
|
|
width := 1200.0
|
|
height := 260.0
|
|
plotWidth := width - 60.0
|
|
startX := 40.0
|
|
maxVal := float64(0)
|
|
for _, e := range plot {
|
|
if float64(e.VmCount) > maxVal {
|
|
maxVal = float64(e.VmCount)
|
|
}
|
|
if float64(e.VcpuTotal) > maxVal {
|
|
maxVal = float64(e.VcpuTotal)
|
|
}
|
|
if float64(e.RamTotalGB) > maxVal {
|
|
maxVal = float64(e.RamTotalGB)
|
|
}
|
|
}
|
|
if maxVal == 0 {
|
|
maxVal = 1
|
|
}
|
|
stepX := plotWidth
|
|
if len(plot) > 1 {
|
|
stepX = plotWidth / float64(len(plot)-1)
|
|
}
|
|
pointsVm := ""
|
|
pointsVcpu := ""
|
|
pointsRam := ""
|
|
for i, e := range plot {
|
|
x := startX + float64(i)*stepX
|
|
yVm := 10 + (1-(float64(e.VmCount)/maxVal))*height
|
|
yVcpu := 10 + (1-(float64(e.VcpuTotal)/maxVal))*height
|
|
yRam := 10 + (1-(float64(e.RamTotalGB)/maxVal))*height
|
|
if i == 0 {
|
|
pointsVm = fmt.Sprintf("%.1f,%.1f", x, yVm)
|
|
pointsVcpu = fmt.Sprintf("%.1f,%.1f", x, yVcpu)
|
|
pointsRam = fmt.Sprintf("%.1f,%.1f", x, yRam)
|
|
} else {
|
|
pointsVm = pointsVm + " " + fmt.Sprintf("%.1f,%.1f", x, yVm)
|
|
pointsVcpu = pointsVcpu + " " + fmt.Sprintf("%.1f,%.1f", x, yVcpu)
|
|
pointsRam = pointsRam + " " + fmt.Sprintf("%.1f,%.1f", x, yRam)
|
|
}
|
|
}
|
|
gridX := []float64{}
|
|
if len(plot) > 1 {
|
|
for i := 0; i < len(plot); i++ {
|
|
gridX = append(gridX, startX+float64(i)*stepX)
|
|
}
|
|
}
|
|
gridY := []float64{}
|
|
for i := 0; i <= 4; i++ {
|
|
gridY = append(gridY, 10+float64(i)*(height/4))
|
|
}
|
|
yTicks := []views.ChartTick{}
|
|
for i := 0; i <= 4; i++ {
|
|
val := maxVal * float64(4-i) / 4
|
|
pos := 10 + float64(i)*(height/4)
|
|
yTicks = append(yTicks, views.ChartTick{Pos: pos, Label: fmt.Sprintf("%.0f", val)})
|
|
}
|
|
xTicks := []views.ChartTick{}
|
|
maxTicks := 6
|
|
stepIdx := 1
|
|
if len(plot) > 1 {
|
|
stepIdx = (len(plot)-1)/maxTicks + 1
|
|
}
|
|
for idx := 0; idx < len(plot); idx += stepIdx {
|
|
x := startX + float64(idx)*stepX
|
|
label := time.Unix(plot[idx].RawTime, 0).Local().Format("01-02 15:04")
|
|
xTicks = append(xTicks, views.ChartTick{Pos: x, Label: label})
|
|
}
|
|
if len(plot) > 1 {
|
|
lastIdx := len(plot) - 1
|
|
xLast := startX + float64(lastIdx)*stepX
|
|
labelLast := time.Unix(plot[lastIdx].RawTime, 0).Local().Format("01-02 15:04")
|
|
if len(xTicks) == 0 || xTicks[len(xTicks)-1].Pos != xLast {
|
|
xTicks = append(xTicks, views.ChartTick{Pos: xLast, Label: labelLast})
|
|
}
|
|
}
|
|
return views.VcenterChartData{
|
|
PointsVm: pointsVm,
|
|
PointsVcpu: pointsVcpu,
|
|
PointsRam: pointsRam,
|
|
Width: int(width),
|
|
Height: int(height),
|
|
GridX: gridX,
|
|
GridY: gridY,
|
|
YTicks: yTicks,
|
|
XTicks: xTicks,
|
|
}
|
|
}
|