265 lines
8.2 KiB
Go
265 lines
8.2 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"vctp/components/views"
|
|
"vctp/db"
|
|
)
|
|
|
|
const (
|
|
vcenterHourlyDetailWindowDays = 45
|
|
vcenterDailyDefaultLimit = 400
|
|
vcenterMonthlyDefaultLimit = 200
|
|
)
|
|
|
|
// 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()
|
|
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/daily?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 keeps backward compatibility with the original endpoint and routes to the new pages.
|
|
// @Summary vCenter totals
|
|
// @Description Redirect-style handler for compatibility; use /vcenters/totals/daily or /vcenters/totals/hourly.
|
|
// @Tags vcenters
|
|
// @Produce text/html
|
|
// @Param vcenter query string true "vCenter URL"
|
|
// @Param type query string false "hourly|daily|monthly"
|
|
// @Param limit query int false "Limit results"
|
|
// @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) {
|
|
switch strings.ToLower(strings.TrimSpace(r.URL.Query().Get("type"))) {
|
|
case "hourly", "hourly-detail", "detail", "detailed":
|
|
h.VcenterTotalsHourlyDetailed(w, r)
|
|
return
|
|
case "monthly":
|
|
h.vcenterTotalsLegacyMonthly(w, r)
|
|
return
|
|
default:
|
|
h.VcenterTotalsDaily(w, r)
|
|
return
|
|
}
|
|
}
|
|
|
|
// VcenterTotalsDaily renders the daily-aggregation totals page for one vCenter.
|
|
// @Summary vCenter daily totals
|
|
// @Description Shows daily aggregated VM count/vCPU/RAM totals for a vCenter.
|
|
// @Tags vcenters
|
|
// @Produce text/html
|
|
// @Param vcenter query string true "vCenter URL"
|
|
// @Param limit query int false "Limit results (default 400)"
|
|
// @Success 200 {string} string "HTML page"
|
|
// @Failure 400 {string} string "Missing vcenter"
|
|
// @Router /vcenters/totals/daily [get]
|
|
func (h *Handler) VcenterTotalsDaily(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
vc, ok := requiredVcenterParam(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
limit := parsePositiveLimit(r, vcenterDailyDefaultLimit)
|
|
rows, err := db.ListVcenterTotalsByType(ctx, h.Database.DB(), vc, "daily", limit)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("failed to list daily totals: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
h.renderVcenterTotalsPage(ctx, w, vc, "daily", rows)
|
|
}
|
|
|
|
// VcenterTotalsHourlyDetailed renders a detailed hourly page over the most recent 45 days.
|
|
// @Summary vCenter hourly totals (45 days)
|
|
// @Description Shows detailed hourly VM count/vCPU/RAM totals for the latest 45 days.
|
|
// @Tags vcenters
|
|
// @Produce text/html
|
|
// @Param vcenter query string true "vCenter URL"
|
|
// @Success 200 {string} string "HTML page"
|
|
// @Failure 400 {string} string "Missing vcenter"
|
|
// @Router /vcenters/totals/hourly [get]
|
|
func (h *Handler) VcenterTotalsHourlyDetailed(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
vc, ok := requiredVcenterParam(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
since := time.Now().AddDate(0, 0, -vcenterHourlyDetailWindowDays)
|
|
rows, err := db.ListVcenterHourlyTotalsSince(ctx, h.Database.DB(), vc, since)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("failed to list hourly totals: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
h.renderVcenterTotalsPage(ctx, w, vc, "hourly45", rows)
|
|
}
|
|
|
|
func (h *Handler) vcenterTotalsLegacyMonthly(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
vc, ok := requiredVcenterParam(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
limit := parsePositiveLimit(r, vcenterMonthlyDefaultLimit)
|
|
rows, err := db.ListVcenterTotalsByType(ctx, h.Database.DB(), vc, "monthly", limit)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("failed to list monthly totals: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
h.renderVcenterTotalsPage(ctx, w, vc, "monthly", rows)
|
|
}
|
|
|
|
func requiredVcenterParam(w http.ResponseWriter, r *http.Request) (string, bool) {
|
|
vc := strings.TrimSpace(r.URL.Query().Get("vcenter"))
|
|
if vc == "" {
|
|
http.Error(w, "vcenter is required", http.StatusBadRequest)
|
|
return "", false
|
|
}
|
|
return vc, true
|
|
}
|
|
|
|
func parsePositiveLimit(r *http.Request, defaultLimit int) int {
|
|
if defaultLimit <= 0 {
|
|
defaultLimit = 200
|
|
}
|
|
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
|
|
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 {
|
|
return parsed
|
|
}
|
|
}
|
|
return defaultLimit
|
|
}
|
|
|
|
func (h *Handler) renderVcenterTotalsPage(ctx context.Context, w http.ResponseWriter, vc string, viewType string, rows []db.VcenterTotalRow) {
|
|
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 = "daily"
|
|
}
|
|
meta := views.VcenterTotalsMeta{
|
|
ViewType: active,
|
|
TypeLabel: "Daily",
|
|
HourlyLink: "/vcenters/totals/hourly?vcenter=" + url.QueryEscape(vcenter),
|
|
DailyLink: "/vcenters/totals/daily?vcenter=" + url.QueryEscape(vcenter),
|
|
HourlyClass: "web3-button",
|
|
DailyClass: "web3-button",
|
|
}
|
|
switch active {
|
|
case "hourly45", "hourly":
|
|
meta.ViewType = "hourly45"
|
|
meta.TypeLabel = fmt.Sprintf("Hourly (last %d days)", vcenterHourlyDetailWindowDays)
|
|
meta.HourlyClass = "web3-button active"
|
|
case "monthly":
|
|
meta.TypeLabel = "Monthly"
|
|
default:
|
|
meta.ViewType = "daily"
|
|
meta.DailyClass = "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),
|
|
}
|
|
}
|