All checks were successful
continuous-integration/drone/push Build is passing
258 lines
7.1 KiB
Go
258 lines
7.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
"vctp/components/views"
|
|
"vctp/db"
|
|
)
|
|
|
|
// VmTrace shows per-snapshot details for a VM across all snapshots.
|
|
// @Summary Trace VM history
|
|
// @Description Shows VM resource history across snapshots, with chart and table.
|
|
// @Tags vm
|
|
// @Produce text/html
|
|
// @Param vm_id query string false "VM ID"
|
|
// @Param vm_uuid query string false "VM UUID"
|
|
// @Param name query string false "VM name"
|
|
// @Success 200 {string} string "HTML page"
|
|
// @Failure 400 {string} string "Missing identifier"
|
|
// @Router /vm/trace [get]
|
|
func (h *Handler) VmTrace(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
vmID := r.URL.Query().Get("vm_id")
|
|
vmUUID := r.URL.Query().Get("vm_uuid")
|
|
name := r.URL.Query().Get("name")
|
|
|
|
var entries []views.VmTraceEntry
|
|
chart := views.VmTraceChart{}
|
|
queryLabel := firstNonEmpty(vmID, vmUUID, name)
|
|
displayQuery := ""
|
|
if queryLabel != "" {
|
|
displayQuery = " for " + queryLabel
|
|
}
|
|
creationLabel := ""
|
|
deletionLabel := ""
|
|
creationApprox := false
|
|
|
|
// Only fetch data when a query is provided; otherwise render empty page with form.
|
|
if vmID != "" || vmUUID != "" || name != "" {
|
|
h.Logger.Info("vm trace request", "vm_id", vmID, "vm_uuid", vmUUID, "name", name)
|
|
lifecycle, lifeErr := db.FetchVmLifecycle(ctx, h.Database.DB(), vmID, vmUUID, name)
|
|
if lifeErr != nil {
|
|
h.Logger.Warn("failed to fetch VM lifecycle", "error", lifeErr)
|
|
}
|
|
rows, err := db.FetchVmTrace(ctx, h.Database.DB(), vmID, vmUUID, name)
|
|
if err != nil {
|
|
h.Logger.Error("failed to fetch VM trace", "error", err)
|
|
http.Error(w, fmt.Sprintf("failed to fetch VM trace: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
h.Logger.Info("vm trace results", "row_count", len(rows))
|
|
entries = make([]views.VmTraceEntry, 0, len(rows))
|
|
for _, row := range rows {
|
|
creation := int64(0)
|
|
if row.CreationTime.Valid {
|
|
creation = row.CreationTime.Int64
|
|
}
|
|
deletion := int64(0)
|
|
if row.DeletionTime.Valid {
|
|
deletion = row.DeletionTime.Int64
|
|
}
|
|
entries = append(entries, views.VmTraceEntry{
|
|
Snapshot: time.Unix(row.SnapshotTime, 0).Local().Format("2006-01-02 15:04:05"),
|
|
RawTime: row.SnapshotTime,
|
|
Name: row.Name,
|
|
VmId: row.VmId,
|
|
VmUuid: row.VmUuid,
|
|
Vcenter: row.Vcenter,
|
|
ResourcePool: row.ResourcePool,
|
|
VcpuCount: row.VcpuCount,
|
|
RamGB: row.RamGB,
|
|
ProvisionedDisk: row.ProvisionedDisk,
|
|
CreationTime: formatMaybeTime(creation),
|
|
DeletionTime: formatMaybeTime(deletion),
|
|
})
|
|
}
|
|
chart = buildVmTraceChart(entries)
|
|
|
|
if len(entries) > 0 {
|
|
if lifecycle.CreationTime > 0 {
|
|
ts := time.Unix(lifecycle.CreationTime, 0).Local().Format("2006-01-02 15:04:05")
|
|
if lifecycle.CreationApprox {
|
|
creationLabel = fmt.Sprintf("%s (approx. earliest snapshot)", ts)
|
|
// dont double up on the approximate text
|
|
//creationApprox = true
|
|
} else {
|
|
creationLabel = ts
|
|
}
|
|
} else {
|
|
creationLabel = time.Unix(entries[0].RawTime, 0).Local().Format("2006-01-02 15:04:05")
|
|
creationApprox = true
|
|
}
|
|
if lifecycle.DeletionTime > 0 {
|
|
deletionLabel = time.Unix(lifecycle.DeletionTime, 0).Local().Format("2006-01-02 15:04:05")
|
|
}
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if err := views.VmTracePage(queryLabel, displayQuery, vmID, vmUUID, name, creationLabel, deletionLabel, creationApprox, entries, chart).Render(ctx, w); err != nil {
|
|
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func buildVmTraceChart(entries []views.VmTraceEntry) views.VmTraceChart {
|
|
if len(entries) == 0 {
|
|
return views.VmTraceChart{}
|
|
}
|
|
maxResource := float64(0)
|
|
for _, e := range entries {
|
|
if float64(e.VcpuCount) > maxResource {
|
|
maxResource = float64(e.VcpuCount)
|
|
}
|
|
if float64(e.RamGB) > maxResource {
|
|
maxResource = float64(e.RamGB)
|
|
}
|
|
}
|
|
if maxResource == 0 {
|
|
maxResource = 1
|
|
}
|
|
|
|
tinLevel := maxResource
|
|
bronzeLevel := maxResource * 0.9
|
|
silverLevel := maxResource * 0.8
|
|
goldLevel := maxResource * 0.7
|
|
|
|
labels := make([]string, 0, len(entries))
|
|
tickLabels := make([]string, 0, len(entries))
|
|
vcpuValues := make([]float64, 0, len(entries))
|
|
ramValues := make([]float64, 0, len(entries))
|
|
tinValues := make([]float64, 0, len(entries))
|
|
bronzeValues := make([]float64, 0, len(entries))
|
|
silverValues := make([]float64, 0, len(entries))
|
|
goldValues := make([]float64, 0, len(entries))
|
|
poolNames := make([]string, 0, len(entries))
|
|
|
|
for _, e := range entries {
|
|
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"))
|
|
vcpuValues = append(vcpuValues, float64(e.VcpuCount))
|
|
ramValues = append(ramValues, float64(e.RamGB))
|
|
|
|
pool := strings.TrimSpace(e.ResourcePool)
|
|
if pool == "" {
|
|
pool = "Unknown"
|
|
}
|
|
poolNames = append(poolNames, pool)
|
|
|
|
lower := strings.ToLower(pool)
|
|
if lower == "tin" {
|
|
tinValues = append(tinValues, tinLevel)
|
|
} else {
|
|
tinValues = append(tinValues, 0)
|
|
}
|
|
if lower == "bronze" {
|
|
bronzeValues = append(bronzeValues, bronzeLevel)
|
|
} else {
|
|
bronzeValues = append(bronzeValues, 0)
|
|
}
|
|
if lower == "silver" {
|
|
silverValues = append(silverValues, silverLevel)
|
|
} else {
|
|
silverValues = append(silverValues, 0)
|
|
}
|
|
if lower == "gold" {
|
|
goldValues = append(goldValues, goldLevel)
|
|
} else {
|
|
goldValues = append(goldValues, 0)
|
|
}
|
|
}
|
|
|
|
cfg := lineChartConfig{
|
|
Height: 360,
|
|
XTicks: 8,
|
|
YTicks: 5,
|
|
YLabel: "Resources / Pool",
|
|
XLabel: "Snapshots (oldest left, newest right)",
|
|
Labels: labels,
|
|
TickLabels: tickLabels,
|
|
Series: []lineChartSeries{
|
|
{
|
|
Name: "vCPU",
|
|
Color: "#2563eb",
|
|
Values: vcpuValues,
|
|
TooltipFormat: "int",
|
|
LineWidth: 2.5,
|
|
},
|
|
{
|
|
Name: "RAM (GB)",
|
|
Color: "#16a34a",
|
|
Values: ramValues,
|
|
TooltipFormat: "int",
|
|
LineWidth: 2.5,
|
|
},
|
|
{
|
|
Name: "Tin",
|
|
Color: "#0ea5e9",
|
|
Values: tinValues,
|
|
Dash: []float64{4, 4},
|
|
LineWidth: 1.5,
|
|
TooltipHidden: true,
|
|
},
|
|
{
|
|
Name: "Bronze",
|
|
Color: "#a855f7",
|
|
Values: bronzeValues,
|
|
Dash: []float64{4, 4},
|
|
LineWidth: 1.5,
|
|
TooltipHidden: true,
|
|
},
|
|
{
|
|
Name: "Silver",
|
|
Color: "#94a3b8",
|
|
Values: silverValues,
|
|
Dash: []float64{4, 4},
|
|
LineWidth: 1.5,
|
|
TooltipHidden: true,
|
|
},
|
|
{
|
|
Name: "Gold",
|
|
Color: "#f59e0b",
|
|
Values: goldValues,
|
|
Dash: []float64{4, 4},
|
|
LineWidth: 1.5,
|
|
TooltipHidden: true,
|
|
},
|
|
},
|
|
HoverRows: []lineChartHoverRow{
|
|
{
|
|
Name: "Resource Pool",
|
|
Values: poolNames,
|
|
},
|
|
},
|
|
}
|
|
return views.VmTraceChart{
|
|
ConfigJSON: encodeLineChartConfig(cfg),
|
|
}
|
|
}
|
|
|
|
func firstNonEmpty(vals ...string) string {
|
|
for _, v := range vals {
|
|
if v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func formatMaybeTime(ts int64) string {
|
|
if ts == 0 {
|
|
return ""
|
|
}
|
|
return time.Unix(ts, 0).Local().Format("2006-01-02 15:04:05")
|
|
}
|