All checks were successful
continuous-integration/drone/push Build is passing
195 lines
5.7 KiB
Go
195 lines
5.7 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])
|
|
}
|
|
|
|
labels := make([]string, 0, len(plot))
|
|
tickLabels := make([]string, 0, len(plot))
|
|
vmValues := make([]float64, 0, len(plot))
|
|
vcpuValues := make([]float64, 0, len(plot))
|
|
ramValues := make([]float64, 0, len(plot))
|
|
|
|
for _, e := range plot {
|
|
t := time.Unix(e.RawTime, 0).Local()
|
|
labels = append(labels, t.Format("2006-01-02 15:04:05"))
|
|
tickLabels = append(tickLabels, t.Format("01-02 15:04"))
|
|
vmValues = append(vmValues, float64(e.VmCount))
|
|
vcpuValues = append(vcpuValues, float64(e.VcpuTotal))
|
|
ramValues = append(ramValues, float64(e.RamTotalGB))
|
|
}
|
|
|
|
cfg := lineChartConfig{
|
|
Height: 360,
|
|
XTicks: 6,
|
|
YTicks: 5,
|
|
YLabel: "Totals",
|
|
XLabel: "Snapshots (oldest left, newest right)",
|
|
Labels: labels,
|
|
TickLabels: tickLabels,
|
|
Series: []lineChartSeries{
|
|
{
|
|
Name: "VMs",
|
|
Color: "#2563eb",
|
|
Values: vmValues,
|
|
TooltipFormat: "int",
|
|
LineWidth: 2.5,
|
|
},
|
|
{
|
|
Name: "vCPU",
|
|
Color: "#16a34a",
|
|
Values: vcpuValues,
|
|
TooltipFormat: "int",
|
|
LineWidth: 2.5,
|
|
},
|
|
{
|
|
Name: "RAM (GB)",
|
|
Color: "#ea580c",
|
|
Values: ramValues,
|
|
TooltipFormat: "int",
|
|
LineWidth: 2.5,
|
|
},
|
|
},
|
|
}
|
|
return views.VcenterChartData{
|
|
ConfigJSON: encodeLineChartConfig(cfg),
|
|
}
|
|
}
|