enhance database logging
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -27,6 +27,7 @@ templ Index(info BuildInfo) {
|
|||||||
<a class="web2-button" href="/snapshots/hourly">Hourly Snapshots</a>
|
<a class="web2-button" href="/snapshots/hourly">Hourly Snapshots</a>
|
||||||
<a class="web2-button" href="/snapshots/daily">Daily Snapshots</a>
|
<a class="web2-button" href="/snapshots/daily">Daily Snapshots</a>
|
||||||
<a class="web2-button" href="/snapshots/monthly">Monthly Snapshots</a>
|
<a class="web2-button" href="/snapshots/monthly">Monthly Snapshots</a>
|
||||||
|
<a class="web2-button" href="/vm/trace">VM Trace</a>
|
||||||
<a class="web2-button" href="/vcenters">vCenters</a>
|
<a class="web2-button" href="/vcenters">vCenters</a>
|
||||||
<a class="web2-button" href="/swagger/">Swagger UI</a>
|
<a class="web2-button" href="/swagger/">Swagger UI</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,14 +47,14 @@ func Index(info BuildInfo) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<body class=\"flex flex-col min-h-screen web2-bg\"><main class=\"flex-grow web2-shell space-y-8\"><section class=\"web2-header\"><div class=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\"><div><div class=\"web2-pill\">vCTP Console</div><h1 class=\"mt-3 text-4xl font-bold\">Chargeback Intelligence Dashboard</h1><p class=\"mt-2 text-sm text-slate-600\">Point in time snapshots of consumption.</p></div><div class=\"web2-button-group\"><a class=\"web2-button\" href=\"/snapshots/hourly\">Hourly Snapshots</a> <a class=\"web2-button\" href=\"/snapshots/daily\">Daily Snapshots</a> <a class=\"web2-button\" href=\"/snapshots/monthly\">Monthly Snapshots</a> <a class=\"web2-button\" href=\"/vcenters\">vCenters</a> <a class=\"web2-button\" href=\"/swagger/\">Swagger UI</a></div></div></section><section class=\"grid gap-6 md:grid-cols-3\"><div class=\"web2-card\"><p class=\"text-xs uppercase tracking-[0.2em] text-slate-400\">Build Time</p><p class=\"mt-3 text-xl font-semibold\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<body class=\"flex flex-col min-h-screen web2-bg\"><main class=\"flex-grow web2-shell space-y-8\"><section class=\"web2-header\"><div class=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\"><div><div class=\"web2-pill\">vCTP Console</div><h1 class=\"mt-3 text-4xl font-bold\">Chargeback Intelligence Dashboard</h1><p class=\"mt-2 text-sm text-slate-600\">Point in time snapshots of consumption.</p></div><div class=\"web2-button-group\"><a class=\"web2-button\" href=\"/snapshots/hourly\">Hourly Snapshots</a> <a class=\"web2-button\" href=\"/snapshots/daily\">Daily Snapshots</a> <a class=\"web2-button\" href=\"/snapshots/monthly\">Monthly Snapshots</a> <a class=\"web2-button\" href=\"/vm/trace\">VM Trace</a> <a class=\"web2-button\" href=\"/vcenters\">vCenters</a> <a class=\"web2-button\" href=\"/swagger/\">Swagger UI</a></div></div></section><section class=\"grid gap-6 md:grid-cols-3\"><div class=\"web2-card\"><p class=\"text-xs uppercase tracking-[0.2em] text-slate-400\">Build Time</p><p class=\"mt-3 text-xl font-semibold\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var2 string
|
var templ_7745c5c3_Var2 string
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(info.BuildTime)
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(info.BuildTime)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 39, Col: 59}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 40, Col: 59}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -67,7 +67,7 @@ func Index(info BuildInfo) templ.Component {
|
|||||||
var templ_7745c5c3_Var3 string
|
var templ_7745c5c3_Var3 string
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(info.SHA1Ver)
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(info.SHA1Ver)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 43, Col: 57}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 44, Col: 57}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -80,7 +80,7 @@ func Index(info BuildInfo) templ.Component {
|
|||||||
var templ_7745c5c3_Var4 string
|
var templ_7745c5c3_Var4 string
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(info.GoVersion)
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(info.GoVersion)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 47, Col: 59}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 48, Col: 59}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|||||||
164
components/views/vm_trace.templ
Normal file
164
components/views/vm_trace.templ
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"vctp/components/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VmTraceEntry struct {
|
||||||
|
Snapshot string
|
||||||
|
RawTime int64
|
||||||
|
Name string
|
||||||
|
VmId string
|
||||||
|
VmUuid string
|
||||||
|
Vcenter string
|
||||||
|
ResourcePool string
|
||||||
|
VcpuCount int64
|
||||||
|
RamGB int64
|
||||||
|
ProvisionedDisk float64
|
||||||
|
CreationTime string
|
||||||
|
DeletionTime string
|
||||||
|
}
|
||||||
|
|
||||||
|
type VmTraceChart struct {
|
||||||
|
PointsVcpu string
|
||||||
|
PointsRam string
|
||||||
|
PointsTin string
|
||||||
|
PointsBronze string
|
||||||
|
PointsSilver string
|
||||||
|
PointsGold string
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
GridX []float64
|
||||||
|
GridY []float64
|
||||||
|
XTicks []ChartTick
|
||||||
|
YTicks []ChartTick
|
||||||
|
}
|
||||||
|
|
||||||
|
templ VmTracePage(query string, display_query string, vm_id string, vm_uuid string, vm_name string, entries []VmTraceEntry, chart VmTraceChart) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
@core.Header()
|
||||||
|
<body class="flex flex-col min-h-screen web2-bg">
|
||||||
|
<main class="flex-grow web2-shell space-y-8 max-w-screen-2xl mx-auto" style="max-width: 1400px;">
|
||||||
|
<section class="web2-header">
|
||||||
|
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="web2-pill">VM Trace</div>
|
||||||
|
<h1 class="mt-3 text-4xl font-bold">Snapshot history{display_query}</h1>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">Timeline of vCPU, RAM, and resource pool changes across snapshots.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 flex-wrap">
|
||||||
|
<a class="web2-button" href="/">Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="get" action="/vm/trace" class="mt-4 grid gap-3 md:grid-cols-3">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label class="text-sm text-slate-600" for="vm_id">VM ID</label>
|
||||||
|
<input class="web2-card border border-slate-200 px-3 py-2 rounded" type="text" id="vm_id" name="vm_id" value={vm_id} placeholder="vm-12345"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label class="text-sm text-slate-600" for="vm_uuid">VM UUID</label>
|
||||||
|
<input class="web2-card border border-slate-200 px-3 py-2 rounded" type="text" id="vm_uuid" name="vm_uuid" value={vm_uuid} placeholder="uuid..."/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label class="text-sm text-slate-600" for="name">Name</label>
|
||||||
|
<input class="web2-card border border-slate-200 px-3 py-2 rounded" type="text" id="name" name="name" value={vm_name} placeholder="VM name"/>
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-3 flex gap-2">
|
||||||
|
<button class="web3-button active" type="submit">Load VM Trace</button>
|
||||||
|
<a class="web3-button" href="/vm/trace">Clear</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="web2-card">
|
||||||
|
<div class="flex items-center justify-between gap-3 mb-4 flex-wrap">
|
||||||
|
<h2 class="text-lg font-semibold">Snapshot Timeline</h2>
|
||||||
|
<span class="web2-badge">{len(entries)} samples</span>
|
||||||
|
</div>
|
||||||
|
if chart.PointsVcpu != "" {
|
||||||
|
<div class="mb-6 overflow-auto">
|
||||||
|
<svg width="100%" height="360" viewBox={"0 0 " + fmt.Sprintf("%d", chart.Width) + " 320"} role="img" aria-label="VM timeline">
|
||||||
|
<rect x="40" y="10" width={fmt.Sprintf("%d", chart.Width-60)} height={fmt.Sprintf("%d", chart.Height)} fill="white" stroke="#e2e8f0"></rect>
|
||||||
|
<g stroke="#e2e8f0" stroke-width="1" stroke-dasharray="2,4">
|
||||||
|
for _, y := range chart.GridY {
|
||||||
|
<line x1="40" y1={fmt.Sprintf("%.1f", y)} x2={fmt.Sprintf("%d", chart.Width-20)} y2={fmt.Sprintf("%.1f", y)} />
|
||||||
|
}
|
||||||
|
for _, x := range chart.GridX {
|
||||||
|
<line x1={fmt.Sprintf("%.1f", x)} y1="10" x2={fmt.Sprintf("%.1f", x)} y2={fmt.Sprintf("%d", chart.Height+10)} />
|
||||||
|
}
|
||||||
|
</g>
|
||||||
|
<line x1="40" y1={fmt.Sprintf("%d", chart.Height+10)} x2={fmt.Sprintf("%d", chart.Width-20)} y2={fmt.Sprintf("%d", chart.Height+10)} stroke="#94a3b8" stroke-width="1.5"></line>
|
||||||
|
<line x1="40" y1="10" x2="40" y2={fmt.Sprintf("%d", chart.Height+10)} stroke="#94a3b8" stroke-width="1.5"></line>
|
||||||
|
<polyline points={chart.PointsVcpu} fill="none" stroke="#2563eb" stroke-width="2.5"></polyline>
|
||||||
|
<polyline points={chart.PointsRam} fill="none" stroke="#16a34a" stroke-width="2.5"></polyline>
|
||||||
|
<polyline points={chart.PointsTin} fill="none" stroke="#0ea5e9" stroke-width="1.5" stroke-dasharray="4,4"></polyline>
|
||||||
|
<polyline points={chart.PointsBronze} fill="none" stroke="#a855f7" stroke-width="1.5" stroke-dasharray="4,4"></polyline>
|
||||||
|
<polyline points={chart.PointsSilver} fill="none" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="4,4"></polyline>
|
||||||
|
<polyline points={chart.PointsGold} fill="none" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="4,4"></polyline>
|
||||||
|
<g font-size="10" fill="#475569" text-anchor="end">
|
||||||
|
for _, tick := range chart.YTicks {
|
||||||
|
<text x="36" y={fmt.Sprintf("%.1f", tick.Pos+3)}>{tick.Label}</text>
|
||||||
|
}
|
||||||
|
</g>
|
||||||
|
<g font-size="10" fill="#475569" text-anchor="middle">
|
||||||
|
for _, tick := range chart.XTicks {
|
||||||
|
<text x={fmt.Sprintf("%.1f", tick.Pos)} y={fmt.Sprintf("%d", chart.Height+24)}>{tick.Label}</text>
|
||||||
|
}
|
||||||
|
</g>
|
||||||
|
<g font-size="12" fill="#475569" transform={"translate(40 " + fmt.Sprintf("%d", chart.Height+50) + ")"}>
|
||||||
|
<rect x="0" y="0" width="14" height="8" fill="#2563eb"></rect><text x="22" y="12">vCPU</text>
|
||||||
|
<rect x="90" y="0" width="14" height="8" fill="#16a34a"></rect><text x="112" y="12">RAM (GB)</text>
|
||||||
|
<rect x="200" y="0" width="14" height="8" fill="#0ea5e9"></rect><text x="222" y="12">Tin</text>
|
||||||
|
<rect x="260" y="0" width="14" height="8" fill="#a855f7"></rect><text x="282" y="12">Bronze</text>
|
||||||
|
<rect x="340" y="0" width="14" height="8" fill="#94a3b8"></rect><text x="362" y="12">Silver</text>
|
||||||
|
<rect x="420" y="0" width="14" height="8" fill="#f59e0b"></rect><text x="442" y="12">Gold</text>
|
||||||
|
</g>
|
||||||
|
<text x="15" y="20" transform={"rotate(-90 15 20)"} font-size="12" fill="#475569">Resources / Pool</text>
|
||||||
|
<text x={fmt.Sprintf("%d", chart.Width/2)} y={fmt.Sprintf("%d", chart.Height+70)} font-size="12" fill="#475569">Snapshots (oldest left, newest right)</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="overflow-hidden border border-slate-200 rounded">
|
||||||
|
<table class="web2-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Snapshot</th>
|
||||||
|
<th>VM Name</th>
|
||||||
|
<th>VmId</th>
|
||||||
|
<th>VmUuid</th>
|
||||||
|
<th>Vcenter</th>
|
||||||
|
<th>Resource Pool</th>
|
||||||
|
<th class="text-right">vCPUs</th>
|
||||||
|
<th class="text-right">RAM (GB)</th>
|
||||||
|
<th class="text-right">Disk</th>
|
||||||
|
<th>Creation</th>
|
||||||
|
<th>Deletion</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
for _, e := range entries {
|
||||||
|
<tr>
|
||||||
|
<td>{e.Snapshot}</td>
|
||||||
|
<td>{e.Name}</td>
|
||||||
|
<td>{e.VmId}</td>
|
||||||
|
<td>{e.VmUuid}</td>
|
||||||
|
<td>{e.Vcenter}</td>
|
||||||
|
<td>{e.ResourcePool}</td>
|
||||||
|
<td class="text-right">{e.VcpuCount}</td>
|
||||||
|
<td class="text-right">{e.RamGB}</td>
|
||||||
|
<td class="text-right">{fmt.Sprintf("%.1f", e.ProvisionedDisk)}</td>
|
||||||
|
<td>{e.CreationTime}</td>
|
||||||
|
<td>{e.DeletionTime}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
@core.Footer()
|
||||||
|
</html>
|
||||||
|
}
|
||||||
719
components/views/vm_trace_templ.go
Normal file
719
components/views/vm_trace_templ.go
Normal file
@@ -0,0 +1,719 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.977
|
||||||
|
package views
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"vctp/components/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VmTraceEntry struct {
|
||||||
|
Snapshot string
|
||||||
|
RawTime int64
|
||||||
|
Name string
|
||||||
|
VmId string
|
||||||
|
VmUuid string
|
||||||
|
Vcenter string
|
||||||
|
ResourcePool string
|
||||||
|
VcpuCount int64
|
||||||
|
RamGB int64
|
||||||
|
ProvisionedDisk float64
|
||||||
|
CreationTime string
|
||||||
|
DeletionTime string
|
||||||
|
}
|
||||||
|
|
||||||
|
type VmTraceChart struct {
|
||||||
|
PointsVcpu string
|
||||||
|
PointsRam string
|
||||||
|
PointsTin string
|
||||||
|
PointsBronze string
|
||||||
|
PointsSilver string
|
||||||
|
PointsGold string
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
GridX []float64
|
||||||
|
GridY []float64
|
||||||
|
XTicks []ChartTick
|
||||||
|
YTicks []ChartTick
|
||||||
|
}
|
||||||
|
|
||||||
|
func VmTracePage(query string, display_query string, vm_id string, vm_uuid string, vm_name string, entries []VmTraceEntry, chart VmTraceChart) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = core.Header().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<body class=\"flex flex-col min-h-screen web2-bg\"><main class=\"flex-grow web2-shell space-y-8 max-w-screen-2xl mx-auto\" style=\"max-width: 1400px;\"><section class=\"web2-header\"><div class=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\"><div><div class=\"web2-pill\">VM Trace</div><h1 class=\"mt-3 text-4xl font-bold\">Snapshot history")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(display_query)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 48, Col: 74}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</h1><p class=\"mt-2 text-sm text-slate-600\">Timeline of vCPU, RAM, and resource pool changes across snapshots.</p></div><div class=\"flex gap-3 flex-wrap\"><a class=\"web2-button\" href=\"/\">Dashboard</a></div></div><form method=\"get\" action=\"/vm/trace\" class=\"mt-4 grid gap-3 md:grid-cols-3\"><div class=\"flex flex-col gap-1\"><label class=\"text-sm text-slate-600\" for=\"vm_id\">VM ID</label> <input class=\"web2-card border border-slate-200 px-3 py-2 rounded\" type=\"text\" id=\"vm_id\" name=\"vm_id\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(vm_id)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 58, Col: 123}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" placeholder=\"vm-12345\"></div><div class=\"flex flex-col gap-1\"><label class=\"text-sm text-slate-600\" for=\"vm_uuid\">VM UUID</label> <input class=\"web2-card border border-slate-200 px-3 py-2 rounded\" type=\"text\" id=\"vm_uuid\" name=\"vm_uuid\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(vm_uuid)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 62, Col: 129}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" placeholder=\"uuid...\"></div><div class=\"flex flex-col gap-1\"><label class=\"text-sm text-slate-600\" for=\"name\">Name</label> <input class=\"web2-card border border-slate-200 px-3 py-2 rounded\" type=\"text\" id=\"name\" name=\"name\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(vm_name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 66, Col: 123}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" placeholder=\"VM name\"></div><div class=\"md:col-span-3 flex gap-2\"><button class=\"web3-button active\" type=\"submit\">Load VM Trace</button> <a class=\"web3-button\" href=\"/vm/trace\">Clear</a></div></form></section><section class=\"web2-card\"><div class=\"flex items-center justify-between gap-3 mb-4 flex-wrap\"><h2 class=\"text-lg font-semibold\">Snapshot Timeline</h2><span class=\"web2-badge\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 78, Col: 44}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " samples</span></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if chart.PointsVcpu != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"mb-6 overflow-auto\"><svg width=\"100%\" height=\"360\" viewBox=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs("0 0 " + fmt.Sprintf("%d", chart.Width) + " 320")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 82, Col: 95}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" role=\"img\" aria-label=\"VM timeline\"><rect x=\"40\" y=\"10\" width=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width-60))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 83, Col: 68}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" height=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 83, Col: 109}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" fill=\"white\" stroke=\"#e2e8f0\"></rect> <g stroke=\"#e2e8f0\" stroke-width=\"1\" stroke-dasharray=\"2,4\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, y := range chart.GridY {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<line x1=\"40\" y1=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 string
|
||||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", y))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 86, Col: 50}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" x2=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var11 string
|
||||||
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width-20))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 86, Col: 89}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" y2=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 string
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", y))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 86, Col: 117}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"></line> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, x := range chart.GridX {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<line x1=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", x))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 89, Col: 42}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" y1=\"10\" x2=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 string
|
||||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", x))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 89, Col: 78}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" y2=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var15 string
|
||||||
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 89, Col: 118}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\"></line>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</g> <line x1=\"40\" y1=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var16 string
|
||||||
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 92, Col: 60}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" x2=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var17 string
|
||||||
|
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width-20))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 92, Col: 99}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" y2=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var18 string
|
||||||
|
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 92, Col: 139}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" stroke=\"#94a3b8\" stroke-width=\"1.5\"></line> <line x1=\"40\" y1=\"10\" x2=\"40\" y2=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var19 string
|
||||||
|
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 93, Col: 76}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" stroke=\"#94a3b8\" stroke-width=\"1.5\"></line> <polyline points=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var20 string
|
||||||
|
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsVcpu)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 94, Col: 42}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" fill=\"none\" stroke=\"#2563eb\" stroke-width=\"2.5\"></polyline> <polyline points=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var21 string
|
||||||
|
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsRam)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 95, Col: 41}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" fill=\"none\" stroke=\"#16a34a\" stroke-width=\"2.5\"></polyline> <polyline points=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var22 string
|
||||||
|
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsTin)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 96, Col: 41}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" fill=\"none\" stroke=\"#0ea5e9\" stroke-width=\"1.5\" stroke-dasharray=\"4,4\"></polyline> <polyline points=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var23 string
|
||||||
|
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsBronze)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 97, Col: 44}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" fill=\"none\" stroke=\"#a855f7\" stroke-width=\"1.5\" stroke-dasharray=\"4,4\"></polyline> <polyline points=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var24 string
|
||||||
|
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsSilver)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 98, Col: 44}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" fill=\"none\" stroke=\"#94a3b8\" stroke-width=\"1.5\" stroke-dasharray=\"4,4\"></polyline> <polyline points=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var25 string
|
||||||
|
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsGold)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 99, Col: 42}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" fill=\"none\" stroke=\"#f59e0b\" stroke-width=\"1.5\" stroke-dasharray=\"4,4\"></polyline> <g font-size=\"10\" fill=\"#475569\" text-anchor=\"end\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, tick := range chart.YTicks {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<text x=\"36\" y=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var26 string
|
||||||
|
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", tick.Pos+3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 102, Col: 57}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var27 string
|
||||||
|
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(tick.Label)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 102, Col: 70}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</text>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</g> <g font-size=\"10\" fill=\"#475569\" text-anchor=\"middle\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, tick := range chart.XTicks {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<text x=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var28 string
|
||||||
|
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", tick.Pos))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 107, Col: 48}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" y=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var29 string
|
||||||
|
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+24))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 107, Col: 87}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var30 string
|
||||||
|
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(tick.Label)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 107, Col: 100}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</text>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</g> <g font-size=\"12\" fill=\"#475569\" transform=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var31 string
|
||||||
|
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs("translate(40 " + fmt.Sprintf("%d", chart.Height+50) + ")")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 110, Col: 110}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\"><rect x=\"0\" y=\"0\" width=\"14\" height=\"8\" fill=\"#2563eb\"></rect><text x=\"22\" y=\"12\">vCPU</text> <rect x=\"90\" y=\"0\" width=\"14\" height=\"8\" fill=\"#16a34a\"></rect><text x=\"112\" y=\"12\">RAM (GB)</text> <rect x=\"200\" y=\"0\" width=\"14\" height=\"8\" fill=\"#0ea5e9\"></rect><text x=\"222\" y=\"12\">Tin</text> <rect x=\"260\" y=\"0\" width=\"14\" height=\"8\" fill=\"#a855f7\"></rect><text x=\"282\" y=\"12\">Bronze</text> <rect x=\"340\" y=\"0\" width=\"14\" height=\"8\" fill=\"#94a3b8\"></rect><text x=\"362\" y=\"12\">Silver</text> <rect x=\"420\" y=\"0\" width=\"14\" height=\"8\" fill=\"#f59e0b\"></rect><text x=\"442\" y=\"12\">Gold</text></g> <text x=\"15\" y=\"20\" transform=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var32 string
|
||||||
|
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs("rotate(-90 15 20)")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 118, Col: 58}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" font-size=\"12\" fill=\"#475569\">Resources / Pool</text> <text x=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var33 string
|
||||||
|
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width/2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 119, Col: 49}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\" y=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var34 string
|
||||||
|
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+70))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 119, Col: 88}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\" font-size=\"12\" fill=\"#475569\">Snapshots (oldest left, newest right)</text></svg></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<div class=\"overflow-hidden border border-slate-200 rounded\"><table class=\"web2-table\"><thead><tr><th>Snapshot</th><th>VM Name</th><th>VmId</th><th>VmUuid</th><th>Vcenter</th><th>Resource Pool</th><th class=\"text-right\">vCPUs</th><th class=\"text-right\">RAM (GB)</th><th class=\"text-right\">Disk</th><th>Creation</th><th>Deletion</th></tr></thead> <tbody>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<tr><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var35 string
|
||||||
|
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(e.Snapshot)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 143, Col: 25}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</td><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var36 string
|
||||||
|
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(e.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 144, Col: 21}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</td><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var37 string
|
||||||
|
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(e.VmId)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 145, Col: 21}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</td><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var38 string
|
||||||
|
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(e.VmUuid)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 146, Col: 23}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</td><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var39 string
|
||||||
|
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(e.Vcenter)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 147, Col: 24}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</td><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var40 string
|
||||||
|
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(e.ResourcePool)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 148, Col: 29}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</td><td class=\"text-right\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var41 string
|
||||||
|
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(e.VcpuCount)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 149, Col: 45}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</td><td class=\"text-right\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var42 string
|
||||||
|
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(e.RamGB)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 150, Col: 41}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</td><td class=\"text-right\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var43 string
|
||||||
|
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", e.ProvisionedDisk))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 151, Col: 72}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</td><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var44 string
|
||||||
|
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(e.CreationTime)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 152, Col: 29}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</td><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var45 string
|
||||||
|
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(e.DeletionTime)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 153, Col: 29}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</td></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</tbody></table></div></section></main></body>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = core.Footer().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</html>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
188
db/helpers.go
188
db/helpers.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ func TableRowCount(ctx context.Context, dbConn *sqlx.DB, table string) (int64, e
|
|||||||
}
|
}
|
||||||
var count int64
|
var count int64
|
||||||
query := fmt.Sprintf(`SELECT COUNT(*) FROM %s`, table)
|
query := fmt.Sprintf(`SELECT COUNT(*) FROM %s`, table)
|
||||||
if err := dbConn.GetContext(ctx, &count, query); err != nil {
|
if err := getLog(ctx, dbConn, &count, query); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return count, nil
|
return count, nil
|
||||||
@@ -52,13 +53,37 @@ func EnsureColumns(ctx context.Context, dbConn *sqlx.DB, tableName string, colum
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func execLog(ctx context.Context, dbConn *sqlx.DB, query string, args ...interface{}) (sql.Result, error) {
|
||||||
|
res, err := dbConn.ExecContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("db exec failed", "query", strings.TrimSpace(query), "error", err)
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLog(ctx context.Context, dbConn *sqlx.DB, dest interface{}, query string, args ...interface{}) error {
|
||||||
|
err := dbConn.GetContext(ctx, dest, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("db get failed", "query", strings.TrimSpace(query), "error", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectLog(ctx context.Context, dbConn *sqlx.DB, dest interface{}, query string, args ...interface{}) error {
|
||||||
|
err := dbConn.SelectContext(ctx, dest, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("db select failed", "query", strings.TrimSpace(query), "error", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// AddColumnIfMissing performs a best-effort ALTER TABLE to add a column, ignoring "already exists".
|
// AddColumnIfMissing performs a best-effort ALTER TABLE to add a column, ignoring "already exists".
|
||||||
func AddColumnIfMissing(ctx context.Context, dbConn *sqlx.DB, tableName string, column ColumnDef) error {
|
func AddColumnIfMissing(ctx context.Context, dbConn *sqlx.DB, tableName string, column ColumnDef) error {
|
||||||
if _, err := SafeTableName(tableName); err != nil {
|
if _, err := SafeTableName(tableName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
query := fmt.Sprintf(`ALTER TABLE %s ADD COLUMN "%s" %s`, tableName, column.Name, column.Type)
|
query := fmt.Sprintf(`ALTER TABLE %s ADD COLUMN "%s" %s`, tableName, column.Name, column.Type)
|
||||||
if _, err := dbConn.ExecContext(ctx, query); err != nil {
|
if _, err := execLog(ctx, dbConn, query); err != nil {
|
||||||
errText := strings.ToLower(err.Error())
|
errText := strings.ToLower(err.Error())
|
||||||
if strings.Contains(errText, "duplicate column") || strings.Contains(errText, "already exists") {
|
if strings.Contains(errText, "duplicate column") || strings.Contains(errText, "already exists") {
|
||||||
return nil
|
return nil
|
||||||
@@ -97,7 +122,7 @@ func TableHasRows(ctx context.Context, dbConn *sqlx.DB, table string) (bool, err
|
|||||||
}
|
}
|
||||||
query := fmt.Sprintf(`SELECT 1 FROM %s LIMIT 1`, table)
|
query := fmt.Sprintf(`SELECT 1 FROM %s LIMIT 1`, table)
|
||||||
var exists int
|
var exists int
|
||||||
if err := dbConn.GetContext(ctx, &exists, query); err != nil {
|
if err := getLog(ctx, dbConn, &exists, query); err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
@@ -116,7 +141,7 @@ func TableExists(ctx context.Context, dbConn *sqlx.DB, table string) bool {
|
|||||||
return err == nil && count > 0
|
return err == nil && count > 0
|
||||||
case "pgx", "postgres":
|
case "pgx", "postgres":
|
||||||
var count int
|
var count int
|
||||||
err := dbConn.GetContext(ctx, &count, `
|
err := getLog(ctx, dbConn, &count, `
|
||||||
SELECT COUNT(1)
|
SELECT COUNT(1)
|
||||||
FROM pg_catalog.pg_tables
|
FROM pg_catalog.pg_tables
|
||||||
WHERE schemaname = 'public' AND tablename = $1
|
WHERE schemaname = 'public' AND tablename = $1
|
||||||
@@ -160,7 +185,7 @@ func ColumnExists(ctx context.Context, dbConn *sqlx.DB, tableName string, column
|
|||||||
return false, rows.Err()
|
return false, rows.Err()
|
||||||
case "pgx", "postgres":
|
case "pgx", "postgres":
|
||||||
var count int
|
var count int
|
||||||
err := dbConn.GetContext(ctx, &count, `
|
err := getLog(ctx, dbConn, &count, `
|
||||||
SELECT COUNT(1)
|
SELECT COUNT(1)
|
||||||
FROM information_schema.columns
|
FROM information_schema.columns
|
||||||
WHERE table_name = $1 AND column_name = $2
|
WHERE table_name = $1 AND column_name = $2
|
||||||
@@ -189,7 +214,7 @@ FROM %s
|
|||||||
`, table)
|
`, table)
|
||||||
|
|
||||||
var totals SnapshotTotals
|
var totals SnapshotTotals
|
||||||
if err := dbConn.GetContext(ctx, &totals, query); err != nil {
|
if err := getLog(ctx, dbConn, &totals, query); err != nil {
|
||||||
return SnapshotTotals{}, err
|
return SnapshotTotals{}, err
|
||||||
}
|
}
|
||||||
return totals, nil
|
return totals, nil
|
||||||
@@ -209,7 +234,7 @@ FROM (
|
|||||||
`, unionQuery)
|
`, unionQuery)
|
||||||
|
|
||||||
var totals SnapshotTotals
|
var totals SnapshotTotals
|
||||||
if err := dbConn.GetContext(ctx, &totals, query); err != nil {
|
if err := getLog(ctx, dbConn, &totals, query); err != nil {
|
||||||
return SnapshotTotals{}, err
|
return SnapshotTotals{}, err
|
||||||
}
|
}
|
||||||
return totals, nil
|
return totals, nil
|
||||||
@@ -274,7 +299,7 @@ func EnsureSnapshotTable(ctx context.Context, dbConn *sqlx.DB, tableName string)
|
|||||||
);`, tableName)
|
);`, tableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := dbConn.ExecContext(ctx, ddl)
|
_, err := execLog(ctx, dbConn, ddl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -303,7 +328,7 @@ func EnsureSnapshotIndexes(ctx context.Context, dbConn *sqlx.DB, tableName strin
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
for _, idx := range indexes {
|
for _, idx := range indexes {
|
||||||
if _, err := dbConn.ExecContext(ctx, idx); err != nil {
|
if _, err := execLog(ctx, dbConn, idx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,7 +347,7 @@ func BackfillSerialColumn(ctx context.Context, dbConn *sqlx.DB, tableName, colum
|
|||||||
`UPDATE %s SET "%s" = nextval(pg_get_serial_sequence('%s','%s')) WHERE "%s" IS NULL`,
|
`UPDATE %s SET "%s" = nextval(pg_get_serial_sequence('%s','%s')) WHERE "%s" IS NULL`,
|
||||||
tableName, columnName, tableName, columnName, columnName,
|
tableName, columnName, tableName, columnName, columnName,
|
||||||
)
|
)
|
||||||
_, err := dbConn.ExecContext(ctx, query)
|
_, err := execLog(ctx, dbConn, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errText := strings.ToLower(err.Error())
|
errText := strings.ToLower(err.Error())
|
||||||
if strings.Contains(errText, "pg_get_serial_sequence") || strings.Contains(errText, "sequence") {
|
if strings.Contains(errText, "pg_get_serial_sequence") || strings.Contains(errText, "sequence") {
|
||||||
@@ -347,7 +372,7 @@ func ApplySQLiteTuning(ctx context.Context, dbConn *sqlx.DB) {
|
|||||||
`PRAGMA optimize;`,
|
`PRAGMA optimize;`,
|
||||||
}
|
}
|
||||||
for _, pragma := range pragmas {
|
for _, pragma := range pragmas {
|
||||||
_, err = dbConn.ExecContext(ctx, pragma)
|
_, err = execLog(ctx, dbConn, pragma)
|
||||||
if logger, ok := ctx.Value("logger").(*slog.Logger); ok && logger != nil {
|
if logger, ok := ctx.Value("logger").(*slog.Logger); ok && logger != nil {
|
||||||
logger.Debug("Applied SQLite tuning pragma", "pragma", pragma, "error", err)
|
logger.Debug("Applied SQLite tuning pragma", "pragma", pragma, "error", err)
|
||||||
}
|
}
|
||||||
@@ -408,10 +433,10 @@ CREATE TABLE IF NOT EXISTS vm_renames (
|
|||||||
"SnapshotTime" BIGINT NOT NULL
|
"SnapshotTime" BIGINT NOT NULL
|
||||||
)`
|
)`
|
||||||
}
|
}
|
||||||
if _, err := dbConn.ExecContext(ctx, identityDDL); err != nil {
|
if _, err := execLog(ctx, dbConn, identityDDL); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := dbConn.ExecContext(ctx, renameDDL); err != nil {
|
if _, err := execLog(ctx, dbConn, renameDDL); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
indexes := []string{
|
indexes := []string{
|
||||||
@@ -421,7 +446,7 @@ CREATE TABLE IF NOT EXISTS vm_renames (
|
|||||||
`CREATE INDEX IF NOT EXISTS vm_renames_vcenter_idx ON vm_renames ("Vcenter","SnapshotTime")`,
|
`CREATE INDEX IF NOT EXISTS vm_renames_vcenter_idx ON vm_renames ("Vcenter","SnapshotTime")`,
|
||||||
}
|
}
|
||||||
for _, idx := range indexes {
|
for _, idx := range indexes {
|
||||||
if _, err := dbConn.ExecContext(ctx, idx); err != nil {
|
if _, err := execLog(ctx, dbConn, idx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -446,7 +471,7 @@ func UpsertVmIdentity(ctx context.Context, dbConn *sqlx.DB, vcenter string, vmId
|
|||||||
LastSeen sql.NullInt64 `db:"LastSeen"`
|
LastSeen sql.NullInt64 `db:"LastSeen"`
|
||||||
}
|
}
|
||||||
var existing identityRow
|
var existing identityRow
|
||||||
err := dbConn.GetContext(ctx, &existing, `
|
err := getLog(ctx, dbConn, &existing, `
|
||||||
SELECT "Name","Cluster","FirstSeen","LastSeen"
|
SELECT "Name","Cluster","FirstSeen","LastSeen"
|
||||||
FROM vm_identity
|
FROM vm_identity
|
||||||
WHERE "Vcenter" = $1 AND "VmId" = $2 AND "VmUuid" = $3
|
WHERE "Vcenter" = $1 AND "VmId" = $2 AND "VmUuid" = $3
|
||||||
@@ -454,7 +479,7 @@ WHERE "Vcenter" = $1 AND "VmId" = $2 AND "VmUuid" = $3
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(strings.ToLower(err.Error()), "no rows") {
|
if strings.Contains(strings.ToLower(err.Error()), "no rows") {
|
||||||
_, err = dbConn.ExecContext(ctx, `
|
_, err = execLog(ctx, dbConn, `
|
||||||
INSERT INTO vm_identity ("VmId","VmUuid","Vcenter","Name","Cluster","FirstSeen","LastSeen")
|
INSERT INTO vm_identity ("VmId","VmUuid","Vcenter","Name","Cluster","FirstSeen","LastSeen")
|
||||||
VALUES ($1,$2,$3,$4,$5,$6,$6)
|
VALUES ($1,$2,$3,$4,$5,$6,$6)
|
||||||
`, keyVmID, keyUuid, vcenter, name, nullString(cluster), snapshotTime.Unix())
|
`, keyVmID, keyUuid, vcenter, name, nullString(cluster), snapshotTime.Unix())
|
||||||
@@ -465,12 +490,12 @@ VALUES ($1,$2,$3,$4,$5,$6,$6)
|
|||||||
|
|
||||||
renamed := !strings.EqualFold(existing.Name, name) || !strings.EqualFold(strings.TrimSpace(existing.Cluster.String), strings.TrimSpace(cluster.String))
|
renamed := !strings.EqualFold(existing.Name, name) || !strings.EqualFold(strings.TrimSpace(existing.Cluster.String), strings.TrimSpace(cluster.String))
|
||||||
if renamed {
|
if renamed {
|
||||||
_, _ = dbConn.ExecContext(ctx, `
|
_, _ = execLog(ctx, dbConn, `
|
||||||
INSERT INTO vm_renames ("VmId","VmUuid","Vcenter","OldName","NewName","OldCluster","NewCluster","SnapshotTime")
|
INSERT INTO vm_renames ("VmId","VmUuid","Vcenter","OldName","NewName","OldCluster","NewCluster","SnapshotTime")
|
||||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
|
||||||
`, keyVmID, keyUuid, vcenter, existing.Name, name, existing.Cluster.String, cluster.String, snapshotTime.Unix())
|
`, keyVmID, keyUuid, vcenter, existing.Name, name, existing.Cluster.String, cluster.String, snapshotTime.Unix())
|
||||||
}
|
}
|
||||||
_, err = dbConn.ExecContext(ctx, `
|
_, err = execLog(ctx, dbConn, `
|
||||||
UPDATE vm_identity
|
UPDATE vm_identity
|
||||||
SET "Name" = $1, "Cluster" = $2, "LastSeen" = $3
|
SET "Name" = $1, "Cluster" = $2, "LastSeen" = $3
|
||||||
WHERE "Vcenter" = $4 AND "VmId" = $5 AND "VmUuid" = $6
|
WHERE "Vcenter" = $4 AND "VmId" = $5 AND "VmUuid" = $6
|
||||||
@@ -511,14 +536,14 @@ CREATE TABLE IF NOT EXISTS vcenter_totals (
|
|||||||
"RamTotalGB" BIGINT NOT NULL
|
"RamTotalGB" BIGINT NOT NULL
|
||||||
);`
|
);`
|
||||||
}
|
}
|
||||||
if _, err := dbConn.ExecContext(ctx, ddl); err != nil {
|
if _, err := execLog(ctx, dbConn, ddl); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
indexes := []string{
|
indexes := []string{
|
||||||
`CREATE INDEX IF NOT EXISTS vcenter_totals_vc_time_idx ON vcenter_totals ("Vcenter","SnapshotTime" DESC)`,
|
`CREATE INDEX IF NOT EXISTS vcenter_totals_vc_time_idx ON vcenter_totals ("Vcenter","SnapshotTime" DESC)`,
|
||||||
}
|
}
|
||||||
for _, idx := range indexes {
|
for _, idx := range indexes {
|
||||||
if _, err := dbConn.ExecContext(ctx, idx); err != nil {
|
if _, err := execLog(ctx, dbConn, idx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,7 +558,7 @@ func InsertVcenterTotals(ctx context.Context, dbConn *sqlx.DB, vcenter string, s
|
|||||||
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err := dbConn.ExecContext(ctx, `
|
_, err := execLog(ctx, dbConn, `
|
||||||
INSERT INTO vcenter_totals ("Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB")
|
INSERT INTO vcenter_totals ("Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB")
|
||||||
VALUES ($1,$2,$3,$4,$5)
|
VALUES ($1,$2,$3,$4,$5)
|
||||||
`, vcenter, snapshotTime.Unix(), vmCount, vcpuTotal, ramTotal)
|
`, vcenter, snapshotTime.Unix(), vmCount, vcpuTotal, ramTotal)
|
||||||
@@ -585,7 +610,7 @@ FROM vcenter_totals
|
|||||||
WHERE "Vcenter" = $1
|
WHERE "Vcenter" = $1
|
||||||
ORDER BY "SnapshotTime" DESC
|
ORDER BY "SnapshotTime" DESC
|
||||||
LIMIT $2`
|
LIMIT $2`
|
||||||
if err := dbConn.SelectContext(ctx, &rows, query, vcenter, limit); err != nil {
|
if err := selectLog(ctx, dbConn, &rows, query, vcenter, limit); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return rows, nil
|
return rows, nil
|
||||||
@@ -623,7 +648,7 @@ LIMIT $2
|
|||||||
TableName string `db:"table_name"`
|
TableName string `db:"table_name"`
|
||||||
SnapshotTime int64 `db:"snapshot_time"`
|
SnapshotTime int64 `db:"snapshot_time"`
|
||||||
}
|
}
|
||||||
if err := dbConn.SelectContext(ctx, ®Rows, query, snapshotType, limit); err != nil {
|
if err := selectLog(ctx, dbConn, ®Rows, query, snapshotType, limit); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,12 +696,87 @@ WHERE "Vcenter" = $1
|
|||||||
query = strings.ReplaceAll(query, "$1", "?")
|
query = strings.ReplaceAll(query, "$1", "?")
|
||||||
}
|
}
|
||||||
var agg summaryAgg
|
var agg summaryAgg
|
||||||
if err := dbConn.GetContext(ctx, &agg, query, vcenter); err != nil {
|
if err := getLog(ctx, dbConn, &agg, query, vcenter); err != nil {
|
||||||
return summaryAgg{}, err
|
return summaryAgg{}, err
|
||||||
}
|
}
|
||||||
return agg, nil
|
return agg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VmTraceRow holds snapshot data for a single VM across tables.
|
||||||
|
type VmTraceRow struct {
|
||||||
|
SnapshotTime int64 `db:"SnapshotTime"`
|
||||||
|
Name string `db:"Name"`
|
||||||
|
Vcenter string `db:"Vcenter"`
|
||||||
|
VmId string `db:"VmId"`
|
||||||
|
VmUuid string `db:"VmUuid"`
|
||||||
|
ResourcePool string `db:"ResourcePool"`
|
||||||
|
VcpuCount int64 `db:"VcpuCount"`
|
||||||
|
RamGB int64 `db:"RamGB"`
|
||||||
|
ProvisionedDisk float64 `db:"ProvisionedDisk"`
|
||||||
|
CreationTime sql.NullInt64 `db:"CreationTime"`
|
||||||
|
DeletionTime sql.NullInt64 `db:"DeletionTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchVmTrace returns combined hourly snapshot records for a VM (by id/uuid/name) ordered by snapshot time.
|
||||||
|
// To avoid SQLite's UNION term limits, this iterates tables one by one and merges in-memory.
|
||||||
|
func FetchVmTrace(ctx context.Context, dbConn *sqlx.DB, vmID, vmUUID, name string) ([]VmTraceRow, error) {
|
||||||
|
var tables []struct {
|
||||||
|
TableName string `db:"table_name"`
|
||||||
|
SnapshotTime int64 `db:"snapshot_time"`
|
||||||
|
}
|
||||||
|
if err := selectLog(ctx, dbConn, &tables, `
|
||||||
|
SELECT table_name, snapshot_time
|
||||||
|
FROM snapshot_registry
|
||||||
|
WHERE snapshot_type = 'hourly'
|
||||||
|
ORDER BY snapshot_time
|
||||||
|
`); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(tables) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]VmTraceRow, 0, len(tables))
|
||||||
|
driver := strings.ToLower(dbConn.DriverName())
|
||||||
|
|
||||||
|
slog.Debug("vm trace scanning tables", "table_count", len(tables), "vm_id", vmID, "vm_uuid", vmUUID, "name", name)
|
||||||
|
|
||||||
|
for _, t := range tables {
|
||||||
|
if err := ValidateTableName(t.TableName); err != nil {
|
||||||
|
slog.Warn("vm trace skipping table (invalid name)", "table", t.TableName, "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
SELECT %d AS "SnapshotTime",
|
||||||
|
"Name","Vcenter","VmId","VmUuid","ResourcePool","VcpuCount","RamGB","ProvisionedDisk",
|
||||||
|
COALESCE("CreationTime",0) AS "CreationTime",
|
||||||
|
COALESCE("DeletionTime",0) AS "DeletionTime"
|
||||||
|
FROM %s
|
||||||
|
WHERE ("VmId" = ? OR "VmUuid" = ? OR lower("Name") = lower(?))
|
||||||
|
`, t.SnapshotTime, t.TableName)
|
||||||
|
args := []interface{}{vmID, vmUUID, name}
|
||||||
|
if driver != "sqlite" {
|
||||||
|
// convert ? to $1 style for postgres/pgx
|
||||||
|
query = strings.Replace(query, "?", "$1", 1)
|
||||||
|
query = strings.Replace(query, "?", "$2", 1)
|
||||||
|
query = strings.Replace(query, "?", "$3", 1)
|
||||||
|
}
|
||||||
|
var tmp []VmTraceRow
|
||||||
|
if err := selectLog(ctx, dbConn, &tmp, query, args...); err != nil {
|
||||||
|
slog.Warn("vm trace query failed for table", "table", t.TableName, "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slog.Debug("vm trace table rows", "table", t.TableName, "snapshot_time", t.SnapshotTime, "rows", len(tmp))
|
||||||
|
rows = append(rows, tmp...)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(rows, func(i, j int) bool {
|
||||||
|
return rows[i].SnapshotTime < rows[j].SnapshotTime
|
||||||
|
})
|
||||||
|
slog.Info("vm trace combined rows", "total_rows", len(rows))
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
// SyncVcenterTotalsFromSnapshots backfills vcenter_totals using hourly snapshot tables in snapshot_registry.
|
// SyncVcenterTotalsFromSnapshots backfills vcenter_totals using hourly snapshot tables in snapshot_registry.
|
||||||
func SyncVcenterTotalsFromSnapshots(ctx context.Context, dbConn *sqlx.DB) error {
|
func SyncVcenterTotalsFromSnapshots(ctx context.Context, dbConn *sqlx.DB) error {
|
||||||
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
||||||
@@ -687,7 +787,7 @@ func SyncVcenterTotalsFromSnapshots(ctx context.Context, dbConn *sqlx.DB) error
|
|||||||
TableName string `db:"table_name"`
|
TableName string `db:"table_name"`
|
||||||
SnapshotTime int64 `db:"snapshot_time"`
|
SnapshotTime int64 `db:"snapshot_time"`
|
||||||
}
|
}
|
||||||
if err := dbConn.SelectContext(ctx, &hourlyTables, `
|
if err := selectLog(ctx, dbConn, &hourlyTables, `
|
||||||
SELECT table_name, snapshot_time
|
SELECT table_name, snapshot_time
|
||||||
FROM snapshot_registry
|
FROM snapshot_registry
|
||||||
WHERE snapshot_type = 'hourly'
|
WHERE snapshot_type = 'hourly'
|
||||||
@@ -715,7 +815,7 @@ GROUP BY "Vcenter"
|
|||||||
RamTotal int64 `db:"ram_total"`
|
RamTotal int64 `db:"ram_total"`
|
||||||
}
|
}
|
||||||
var aggs []aggRow
|
var aggs []aggRow
|
||||||
if err := dbConn.SelectContext(ctx, &aggs, query); err != nil {
|
if err := selectLog(ctx, dbConn, &aggs, query); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, a := range aggs {
|
for _, a := range aggs {
|
||||||
@@ -730,7 +830,9 @@ WHERE NOT EXISTS (
|
|||||||
if driver == "sqlite" {
|
if driver == "sqlite" {
|
||||||
insert = strings.ReplaceAll(insert, "$", "?")
|
insert = strings.ReplaceAll(insert, "$", "?")
|
||||||
}
|
}
|
||||||
_, _ = dbConn.ExecContext(ctx, insert, a.Vcenter, ht.SnapshotTime, a.VmCount, a.VcpuTotal, a.RamTotal)
|
if _, err := execLog(ctx, dbConn, insert, a.Vcenter, ht.SnapshotTime, a.VmCount, a.VcpuTotal, a.RamTotal); err != nil {
|
||||||
|
slog.Warn("failed to backfill vcenter_totals", "table", ht.TableName, "vcenter", a.Vcenter, "snapshot_time", ht.SnapshotTime, "error", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -745,7 +847,9 @@ func AnalyzeTableIfPostgres(ctx context.Context, dbConn *sqlx.DB, tableName stri
|
|||||||
if driver != "pgx" && driver != "postgres" {
|
if driver != "pgx" && driver != "postgres" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, _ = dbConn.ExecContext(ctx, fmt.Sprintf(`ANALYZE %s`, tableName))
|
if _, err := execLog(ctx, dbConn, fmt.Sprintf(`ANALYZE %s`, tableName)); err != nil {
|
||||||
|
slog.Warn("failed to ANALYZE table", "table", tableName, "error", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPostgresWorkMem sets a per-session work_mem for heavy aggregations; no-op for other drivers.
|
// SetPostgresWorkMem sets a per-session work_mem for heavy aggregations; no-op for other drivers.
|
||||||
@@ -757,7 +861,9 @@ func SetPostgresWorkMem(ctx context.Context, dbConn *sqlx.DB, workMemMB int) {
|
|||||||
if driver != "pgx" && driver != "postgres" {
|
if driver != "pgx" && driver != "postgres" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, _ = dbConn.ExecContext(ctx, fmt.Sprintf(`SET LOCAL work_mem = '%dMB'`, workMemMB))
|
if _, err := execLog(ctx, dbConn, fmt.Sprintf(`SET LOCAL work_mem = '%dMB'`, workMemMB)); err != nil {
|
||||||
|
slog.Warn("failed to set work_mem", "work_mem_mb", workMemMB, "error", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckMigrationState ensures goose migrations are present and not dirty.
|
// CheckMigrationState ensures goose migrations are present and not dirty.
|
||||||
@@ -766,14 +872,14 @@ func CheckMigrationState(ctx context.Context, dbConn *sqlx.DB) error {
|
|||||||
var tableExists bool
|
var tableExists bool
|
||||||
switch driver {
|
switch driver {
|
||||||
case "sqlite":
|
case "sqlite":
|
||||||
err := dbConn.GetContext(ctx, &tableExists, `
|
err := getLog(ctx, dbConn, &tableExists, `
|
||||||
SELECT COUNT(1) > 0 FROM sqlite_master WHERE type='table' AND name='goose_db_version'
|
SELECT COUNT(1) > 0 FROM sqlite_master WHERE type='table' AND name='goose_db_version'
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "pgx", "postgres":
|
case "pgx", "postgres":
|
||||||
err := dbConn.GetContext(ctx, &tableExists, `
|
err := getLog(ctx, dbConn, &tableExists, `
|
||||||
SELECT EXISTS (
|
SELECT EXISTS (
|
||||||
SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'goose_db_version'
|
SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'goose_db_version'
|
||||||
)
|
)
|
||||||
@@ -790,7 +896,7 @@ SELECT EXISTS (
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dirty bool
|
var dirty bool
|
||||||
err := dbConn.GetContext(ctx, &dirty, `
|
err := getLog(ctx, dbConn, &dirty, `
|
||||||
SELECT NOT is_applied
|
SELECT NOT is_applied
|
||||||
FROM goose_db_version
|
FROM goose_db_version
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
@@ -1061,7 +1167,7 @@ WHERE EXISTS (
|
|||||||
`, unionQuery, summaryTable)
|
`, unionQuery, summaryTable)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := dbConn.ExecContext(ctx, sql)
|
_, err := execLog(ctx, dbConn, sql)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1230,13 +1336,13 @@ func EnsureSummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName string)
|
|||||||
);`, tableName)
|
);`, tableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := dbConn.ExecContext(ctx, ddl); err != nil {
|
if _, err := execLog(ctx, dbConn, ddl); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best-effort: drop legacy IsPresent column if it exists.
|
// Best-effort: drop legacy IsPresent column if it exists.
|
||||||
if hasIsPresent, err := ColumnExists(ctx, dbConn, tableName, "IsPresent"); err == nil && hasIsPresent {
|
if hasIsPresent, err := ColumnExists(ctx, dbConn, tableName, "IsPresent"); err == nil && hasIsPresent {
|
||||||
_, _ = dbConn.ExecContext(ctx, fmt.Sprintf(`ALTER TABLE %s DROP COLUMN "IsPresent"`, tableName))
|
_, _ = execLog(ctx, dbConn, fmt.Sprintf(`ALTER TABLE %s DROP COLUMN "IsPresent"`, tableName))
|
||||||
}
|
}
|
||||||
|
|
||||||
indexes := []string{
|
indexes := []string{
|
||||||
@@ -1250,7 +1356,7 @@ func EnsureSummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName string)
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
for _, idx := range indexes {
|
for _, idx := range indexes {
|
||||||
if _, err := dbConn.ExecContext(ctx, idx); err != nil {
|
if _, err := execLog(ctx, dbConn, idx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1283,7 +1389,7 @@ CREATE TABLE IF NOT EXISTS snapshot_runs (
|
|||||||
);
|
);
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
if _, err := dbConn.ExecContext(ctx, ddl); err != nil {
|
if _, err := execLog(ctx, dbConn, ddl); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
indexes := []string{
|
indexes := []string{
|
||||||
@@ -1291,7 +1397,7 @@ CREATE TABLE IF NOT EXISTS snapshot_runs (
|
|||||||
`CREATE INDEX IF NOT EXISTS snapshot_runs_success_idx ON snapshot_runs ("Success")`,
|
`CREATE INDEX IF NOT EXISTS snapshot_runs_success_idx ON snapshot_runs ("Success")`,
|
||||||
}
|
}
|
||||||
for _, idx := range indexes {
|
for _, idx := range indexes {
|
||||||
if _, err := dbConn.ExecContext(ctx, idx); err != nil {
|
if _, err := execLog(ctx, dbConn, idx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1311,7 +1417,7 @@ func UpsertSnapshotRun(ctx context.Context, dbConn *sqlx.DB, vcenter string, sna
|
|||||||
driver := strings.ToLower(dbConn.DriverName())
|
driver := strings.ToLower(dbConn.DriverName())
|
||||||
switch driver {
|
switch driver {
|
||||||
case "sqlite":
|
case "sqlite":
|
||||||
_, err := dbConn.ExecContext(ctx, `
|
_, err := execLog(ctx, dbConn, `
|
||||||
INSERT INTO snapshot_runs ("Vcenter","SnapshotTime","Attempts","Success","LastError","LastAttempt")
|
INSERT INTO snapshot_runs ("Vcenter","SnapshotTime","Attempts","Success","LastError","LastAttempt")
|
||||||
VALUES (?, ?, 1, ?, ?, ?)
|
VALUES (?, ?, 1, ?, ?, ?)
|
||||||
ON CONFLICT("Vcenter","SnapshotTime") DO UPDATE SET
|
ON CONFLICT("Vcenter","SnapshotTime") DO UPDATE SET
|
||||||
@@ -1322,7 +1428,7 @@ ON CONFLICT("Vcenter","SnapshotTime") DO UPDATE SET
|
|||||||
`, vcenter, snapshotTime.Unix(), successStr, errMsg, now)
|
`, vcenter, snapshotTime.Unix(), successStr, errMsg, now)
|
||||||
return err
|
return err
|
||||||
case "pgx", "postgres":
|
case "pgx", "postgres":
|
||||||
_, err := dbConn.ExecContext(ctx, `
|
_, err := execLog(ctx, dbConn, `
|
||||||
INSERT INTO snapshot_runs ("Vcenter","SnapshotTime","Attempts","Success","LastError","LastAttempt")
|
INSERT INTO snapshot_runs ("Vcenter","SnapshotTime","Attempts","Success","LastError","LastAttempt")
|
||||||
VALUES ($1, $2, 1, $3, $4, $5)
|
VALUES ($1, $2, 1, $3, $4, $5)
|
||||||
ON CONFLICT("Vcenter","SnapshotTime") DO UPDATE SET
|
ON CONFLICT("Vcenter","SnapshotTime") DO UPDATE SET
|
||||||
@@ -1368,7 +1474,7 @@ ORDER BY "LastAttempt" ASC
|
|||||||
Attempts int `db:"Attempts"`
|
Attempts int `db:"Attempts"`
|
||||||
}
|
}
|
||||||
rows := []row{}
|
rows := []row{}
|
||||||
if err := dbConn.SelectContext(ctx, &rows, query, args...); err != nil {
|
if err := selectLog(ctx, dbConn, &rows, query, args...); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
results := make([]struct {
|
results := make([]struct {
|
||||||
|
|||||||
@@ -97,10 +97,15 @@ CREATE TABLE IF NOT EXISTS snapshot_registry (
|
|||||||
}
|
}
|
||||||
_, err = dbConn.ExecContext(ctx, `ALTER TABLE snapshot_registry ADD COLUMN snapshot_count BIGINT NOT NULL DEFAULT 0`)
|
_, err = dbConn.ExecContext(ctx, `ALTER TABLE snapshot_registry ADD COLUMN snapshot_count BIGINT NOT NULL DEFAULT 0`)
|
||||||
if err != nil && !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
if err != nil && !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||||
|
slog.Warn("failed to add snapshot_count column", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, _ = dbConn.ExecContext(ctx, `CREATE INDEX IF NOT EXISTS idx_snapshot_registry_type_time ON snapshot_registry (snapshot_type, snapshot_time)`)
|
if _, err := dbConn.ExecContext(ctx, `CREATE INDEX IF NOT EXISTS idx_snapshot_registry_type_time ON snapshot_registry (snapshot_type, snapshot_time)`); err != nil {
|
||||||
_, _ = dbConn.ExecContext(ctx, `CREATE INDEX IF NOT EXISTS idx_snapshot_registry_table_name ON snapshot_registry (table_name)`)
|
slog.Warn("failed to create snapshot_registry index", "error", err)
|
||||||
|
}
|
||||||
|
if _, err := dbConn.ExecContext(ctx, `CREATE INDEX IF NOT EXISTS idx_snapshot_registry_table_name ON snapshot_registry (table_name)`); err != nil {
|
||||||
|
slog.Warn("failed to create snapshot_registry index", "error", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
case "pgx", "postgres":
|
case "pgx", "postgres":
|
||||||
_, err := dbConn.ExecContext(ctx, `
|
_, err := dbConn.ExecContext(ctx, `
|
||||||
@@ -117,10 +122,15 @@ CREATE TABLE IF NOT EXISTS snapshot_registry (
|
|||||||
}
|
}
|
||||||
_, err = dbConn.ExecContext(ctx, `ALTER TABLE snapshot_registry ADD COLUMN snapshot_count BIGINT NOT NULL DEFAULT 0`)
|
_, err = dbConn.ExecContext(ctx, `ALTER TABLE snapshot_registry ADD COLUMN snapshot_count BIGINT NOT NULL DEFAULT 0`)
|
||||||
if err != nil && !strings.Contains(strings.ToLower(err.Error()), "column \"snapshot_count\" of relation \"snapshot_registry\" already exists") {
|
if err != nil && !strings.Contains(strings.ToLower(err.Error()), "column \"snapshot_count\" of relation \"snapshot_registry\" already exists") {
|
||||||
|
slog.Warn("failed to add snapshot_count column", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, _ = dbConn.ExecContext(ctx, `CREATE INDEX IF NOT EXISTS idx_snapshot_registry_type_time ON snapshot_registry (snapshot_type, snapshot_time DESC)`)
|
if _, err := dbConn.ExecContext(ctx, `CREATE INDEX IF NOT EXISTS idx_snapshot_registry_type_time ON snapshot_registry (snapshot_type, snapshot_time DESC)`); err != nil {
|
||||||
_, _ = dbConn.ExecContext(ctx, `CREATE INDEX IF NOT EXISTS idx_snapshot_registry_table_name ON snapshot_registry (table_name)`)
|
slog.Warn("failed to create snapshot_registry index", "error", err)
|
||||||
|
}
|
||||||
|
if _, err := dbConn.ExecContext(ctx, `CREATE INDEX IF NOT EXISTS idx_snapshot_registry_table_name ON snapshot_registry (table_name)`); err != nil {
|
||||||
|
slog.Warn("failed to create snapshot_registry index", "error", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported driver for snapshot registry: %s", driver)
|
return fmt.Errorf("unsupported driver for snapshot registry: %s", driver)
|
||||||
|
|||||||
@@ -808,7 +808,9 @@ func (c *CronTask) captureHourlySnapshotForVcenter(ctx context.Context, startTim
|
|||||||
vc := vcenter.New(c.Logger, c.VcCreds)
|
vc := vcenter.New(c.Logger, c.VcCreds)
|
||||||
if err := vc.Login(url); err != nil {
|
if err := vc.Login(url); err != nil {
|
||||||
metrics.RecordVcenterSnapshot(url, time.Since(started), 0, err)
|
metrics.RecordVcenterSnapshot(url, time.Since(started), 0, err)
|
||||||
_ = db.UpsertSnapshotRun(ctx, c.Database.DB(), url, startTime, false, err.Error())
|
if upErr := db.UpsertSnapshotRun(ctx, c.Database.DB(), url, startTime, false, err.Error()); upErr != nil {
|
||||||
|
c.Logger.Warn("failed to record snapshot run", "url", url, "error", upErr)
|
||||||
|
}
|
||||||
return fmt.Errorf("unable to connect to vcenter: %w", err)
|
return fmt.Errorf("unable to connect to vcenter: %w", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -820,7 +822,9 @@ func (c *CronTask) captureHourlySnapshotForVcenter(ctx context.Context, startTim
|
|||||||
vcVms, err := vc.GetAllVMsWithProps()
|
vcVms, err := vc.GetAllVMsWithProps()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
metrics.RecordVcenterSnapshot(url, time.Since(started), 0, err)
|
metrics.RecordVcenterSnapshot(url, time.Since(started), 0, err)
|
||||||
_ = db.UpsertSnapshotRun(ctx, c.Database.DB(), url, startTime, false, err.Error())
|
if upErr := db.UpsertSnapshotRun(ctx, c.Database.DB(), url, startTime, false, err.Error()); upErr != nil {
|
||||||
|
c.Logger.Warn("failed to record snapshot run", "url", url, "error", upErr)
|
||||||
|
}
|
||||||
return fmt.Errorf("unable to get VMs from vcenter: %w", err)
|
return fmt.Errorf("unable to get VMs from vcenter: %w", err)
|
||||||
}
|
}
|
||||||
c.Logger.Debug("retrieved VMs from vcenter", "url", url, "vm_count", len(vcVms))
|
c.Logger.Debug("retrieved VMs from vcenter", "url", url, "vm_count", len(vcVms))
|
||||||
@@ -895,7 +899,9 @@ func (c *CronTask) captureHourlySnapshotForVcenter(ctx context.Context, startTim
|
|||||||
c.Logger.Error("unable to build snapshot for VM", "vm_id", vm.Reference().Value, "error", err)
|
c.Logger.Error("unable to build snapshot for VM", "vm_id", vm.Reference().Value, "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_ = db.UpsertVmIdentity(ctx, dbConn, url, row.VmId, row.VmUuid, row.Name, row.Cluster, startTime)
|
if err := db.UpsertVmIdentity(ctx, dbConn, url, row.VmId, row.VmUuid, row.Name, row.Cluster, startTime); err != nil {
|
||||||
|
c.Logger.Warn("failed to upsert vm identity", "vcenter", url, "vm_id", row.VmId, "vm_uuid", row.VmUuid, "name", row.Name, "error", err)
|
||||||
|
}
|
||||||
presentSnapshots[vm.Reference().Value] = row
|
presentSnapshots[vm.Reference().Value] = row
|
||||||
if row.VmUuid.Valid {
|
if row.VmUuid.Valid {
|
||||||
presentByUuid[row.VmUuid.String] = struct{}{}
|
presentByUuid[row.VmUuid.String] = struct{}{}
|
||||||
@@ -972,11 +978,15 @@ func (c *CronTask) captureHourlySnapshotForVcenter(ctx context.Context, startTim
|
|||||||
|
|
||||||
if err := insertHourlyBatch(ctx, dbConn, tableName, batch); err != nil {
|
if err := insertHourlyBatch(ctx, dbConn, tableName, batch); err != nil {
|
||||||
metrics.RecordVcenterSnapshot(url, time.Since(started), totals.VmCount, err)
|
metrics.RecordVcenterSnapshot(url, time.Since(started), totals.VmCount, err)
|
||||||
_ = db.UpsertSnapshotRun(ctx, c.Database.DB(), url, startTime, false, err.Error())
|
if upErr := db.UpsertSnapshotRun(ctx, c.Database.DB(), url, startTime, false, err.Error()); upErr != nil {
|
||||||
|
c.Logger.Warn("failed to record snapshot run", "url", url, "error", upErr)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Record per-vCenter totals snapshot.
|
// Record per-vCenter totals snapshot.
|
||||||
_ = db.InsertVcenterTotals(ctx, dbConn, url, startTime, totals.VmCount, totals.VcpuTotal, totals.RamTotal)
|
if err := db.InsertVcenterTotals(ctx, dbConn, url, startTime, totals.VmCount, totals.VcpuTotal, totals.RamTotal); err != nil {
|
||||||
|
slog.Warn("failed to insert vcenter totals", "vcenter", url, "snapshot_time", startTime.Unix(), "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Compare with previous snapshot for this vcenter to mark deletions at snapshot time.
|
// Compare with previous snapshot for this vcenter to mark deletions at snapshot time.
|
||||||
if prevTable, err := latestHourlySnapshotBefore(ctx, dbConn, startTime); err == nil && prevTable != "" {
|
if prevTable, err := latestHourlySnapshotBefore(ctx, dbConn, startTime); err == nil && prevTable != "" {
|
||||||
@@ -995,7 +1005,9 @@ func (c *CronTask) captureHourlySnapshotForVcenter(ctx context.Context, startTim
|
|||||||
"missing_marked", missingCount,
|
"missing_marked", missingCount,
|
||||||
)
|
)
|
||||||
metrics.RecordVcenterSnapshot(url, time.Since(started), totals.VmCount, nil)
|
metrics.RecordVcenterSnapshot(url, time.Since(started), totals.VmCount, nil)
|
||||||
_ = db.UpsertSnapshotRun(ctx, c.Database.DB(), url, startTime, true, "")
|
if upErr := db.UpsertSnapshotRun(ctx, c.Database.DB(), url, startTime, true, ""); upErr != nil {
|
||||||
|
c.Logger.Warn("failed to record snapshot run", "url", url, "error", upErr)
|
||||||
|
}
|
||||||
if deletionsMarked {
|
if deletionsMarked {
|
||||||
if err := c.generateReport(ctx, tableName); err != nil {
|
if err := c.generateReport(ctx, tableName); err != nil {
|
||||||
c.Logger.Warn("failed to regenerate hourly report after deletions", "error", err, "table", tableName)
|
c.Logger.Warn("failed to regenerate hourly report after deletions", "error", err, "table", tableName)
|
||||||
|
|||||||
@@ -19,14 +19,3 @@ type Handler struct {
|
|||||||
Secret *secrets.Secrets
|
Secret *secrets.Secrets
|
||||||
Settings *settings.Settings
|
Settings *settings.Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
func (h *Handler) html(ctx context.Context, w http.ResponseWriter, status int, t templ.Component) {
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
w.WriteHeader(status)
|
|
||||||
|
|
||||||
if err := t.Render(ctx, w); err != nil {
|
|
||||||
h.Logger.Error("Failed to render component", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ import (
|
|||||||
// @Router /vcenters [get]
|
// @Router /vcenters [get]
|
||||||
func (h *Handler) VcenterList(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) VcenterList(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
_ = db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB())
|
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())
|
vcs, err := db.ListVcenters(ctx, h.Database.DB())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("failed to list vcenters: %v", err), http.StatusInternalServerError)
|
http.Error(w, fmt.Sprintf("failed to list vcenters: %v", err), http.StatusInternalServerError)
|
||||||
@@ -67,7 +69,9 @@ func (h *Handler) VcenterTotals(w http.ResponseWriter, r *http.Request) {
|
|||||||
viewType = "hourly"
|
viewType = "hourly"
|
||||||
}
|
}
|
||||||
if viewType == "hourly" {
|
if viewType == "hourly" {
|
||||||
_ = db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB())
|
if err := db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB()); err != nil {
|
||||||
|
h.Logger.Warn("failed to sync vcenter totals", "error", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
limit := 200
|
limit := 200
|
||||||
if l := r.URL.Query().Get("limit"); l != "" {
|
if l := r.URL.Query().Get("limit"); l != "" {
|
||||||
|
|||||||
210
server/handler/vmTrace.go
Normal file
210
server/handler/vmTrace.go
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
if err := views.VmTracePage(queryLabel, displayQuery, vmID, vmUUID, name, 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{}
|
||||||
|
}
|
||||||
|
width := 1200.0
|
||||||
|
height := 220.0
|
||||||
|
plotWidth := width - 60.0
|
||||||
|
startX := 40.0
|
||||||
|
maxVal := float64(0)
|
||||||
|
for _, e := range entries {
|
||||||
|
if float64(e.VcpuCount) > maxVal {
|
||||||
|
maxVal = float64(e.VcpuCount)
|
||||||
|
}
|
||||||
|
if float64(e.RamGB) > maxVal {
|
||||||
|
maxVal = float64(e.RamGB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if maxVal == 0 {
|
||||||
|
maxVal = 1
|
||||||
|
}
|
||||||
|
stepX := plotWidth
|
||||||
|
if len(entries) > 1 {
|
||||||
|
stepX = plotWidth / float64(len(entries)-1)
|
||||||
|
}
|
||||||
|
scale := height / maxVal
|
||||||
|
var ptsVcpu, ptsRam, ptsTin, ptsBronze, ptsSilver, ptsGold string
|
||||||
|
appendPt := func(s string, x, y float64) string {
|
||||||
|
if s == "" {
|
||||||
|
return fmt.Sprintf("%.1f,%.1f", x, y)
|
||||||
|
}
|
||||||
|
return s + " " + fmt.Sprintf("%.1f,%.1f", x, y)
|
||||||
|
}
|
||||||
|
for i, e := range entries {
|
||||||
|
x := startX + float64(i)*stepX
|
||||||
|
yVcpu := 10 + height - float64(e.VcpuCount)*scale
|
||||||
|
yRam := 10 + height - float64(e.RamGB)*scale
|
||||||
|
ptsVcpu = appendPt(ptsVcpu, x, yVcpu)
|
||||||
|
ptsRam = appendPt(ptsRam, x, yRam)
|
||||||
|
poolY := map[string]float64{
|
||||||
|
"tin": 10 + height - scale*maxVal,
|
||||||
|
"bronze": 10 + height - scale*maxVal*0.9,
|
||||||
|
"silver": 10 + height - scale*maxVal*0.8,
|
||||||
|
"gold": 10 + height - scale*maxVal*0.7,
|
||||||
|
}
|
||||||
|
lower := strings.ToLower(e.ResourcePool)
|
||||||
|
if lower == "tin" {
|
||||||
|
ptsTin = appendPt(ptsTin, x, poolY["tin"])
|
||||||
|
} else {
|
||||||
|
ptsTin = appendPt(ptsTin, x, 10+height)
|
||||||
|
}
|
||||||
|
if lower == "bronze" {
|
||||||
|
ptsBronze = appendPt(ptsBronze, x, poolY["bronze"])
|
||||||
|
} else {
|
||||||
|
ptsBronze = appendPt(ptsBronze, x, 10+height)
|
||||||
|
}
|
||||||
|
if lower == "silver" {
|
||||||
|
ptsSilver = appendPt(ptsSilver, x, poolY["silver"])
|
||||||
|
} else {
|
||||||
|
ptsSilver = appendPt(ptsSilver, x, 10+height)
|
||||||
|
}
|
||||||
|
if lower == "gold" {
|
||||||
|
ptsGold = appendPt(ptsGold, x, poolY["gold"])
|
||||||
|
} else {
|
||||||
|
ptsGold = appendPt(ptsGold, x, 10+height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gridY := []float64{}
|
||||||
|
for i := 0; i <= 4; i++ {
|
||||||
|
gridY = append(gridY, 10+float64(i)*(height/4))
|
||||||
|
}
|
||||||
|
gridX := []float64{}
|
||||||
|
for i := 0; i < len(entries); i++ {
|
||||||
|
gridX = append(gridX, startX+float64(i)*stepX)
|
||||||
|
}
|
||||||
|
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 := 8
|
||||||
|
stepIdx := 1
|
||||||
|
if len(entries) > 1 {
|
||||||
|
stepIdx = (len(entries)-1)/maxTicks + 1
|
||||||
|
}
|
||||||
|
for idx := 0; idx < len(entries); idx += stepIdx {
|
||||||
|
x := startX + float64(idx)*stepX
|
||||||
|
label := time.Unix(entries[idx].RawTime, 0).Local().Format("01-02 15:04")
|
||||||
|
xTicks = append(xTicks, views.ChartTick{Pos: x, Label: label})
|
||||||
|
}
|
||||||
|
if len(entries) > 1 {
|
||||||
|
lastIdx := len(entries) - 1
|
||||||
|
xLast := startX + float64(lastIdx)*stepX
|
||||||
|
labelLast := time.Unix(entries[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.VmTraceChart{
|
||||||
|
PointsVcpu: ptsVcpu,
|
||||||
|
PointsRam: ptsRam,
|
||||||
|
PointsTin: ptsTin,
|
||||||
|
PointsBronze: ptsBronze,
|
||||||
|
PointsSilver: ptsSilver,
|
||||||
|
PointsGold: ptsGold,
|
||||||
|
Width: int(width),
|
||||||
|
Height: int(height),
|
||||||
|
GridX: gridX,
|
||||||
|
GridY: gridY,
|
||||||
|
XTicks: xTicks,
|
||||||
|
YTicks: yTicks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
@@ -921,6 +921,52 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/vm/trace": {
|
||||||
|
"get": {
|
||||||
|
"description": "Shows VM resource history across snapshots, with chart and table.",
|
||||||
|
"produces": [
|
||||||
|
"text/html"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"vm"
|
||||||
|
],
|
||||||
|
"summary": "Trace VM history",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "VM ID",
|
||||||
|
"name": "vm_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "VM UUID",
|
||||||
|
"name": "vm_uuid",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "VM name",
|
||||||
|
"name": "name",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "HTML page",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Missing identifier",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
|||||||
@@ -910,6 +910,52 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/vm/trace": {
|
||||||
|
"get": {
|
||||||
|
"description": "Shows VM resource history across snapshots, with chart and table.",
|
||||||
|
"produces": [
|
||||||
|
"text/html"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"vm"
|
||||||
|
],
|
||||||
|
"summary": "Trace VM history",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "VM ID",
|
||||||
|
"name": "vm_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "VM UUID",
|
||||||
|
"name": "vm_uuid",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "VM name",
|
||||||
|
"name": "name",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "HTML page",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Missing identifier",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
|||||||
@@ -764,4 +764,34 @@ paths:
|
|||||||
summary: vCenter totals
|
summary: vCenter totals
|
||||||
tags:
|
tags:
|
||||||
- vcenters
|
- vcenters
|
||||||
|
/vm/trace:
|
||||||
|
get:
|
||||||
|
description: Shows VM resource history across snapshots, with chart and table.
|
||||||
|
parameters:
|
||||||
|
- description: VM ID
|
||||||
|
in: query
|
||||||
|
name: vm_id
|
||||||
|
type: string
|
||||||
|
- description: VM UUID
|
||||||
|
in: query
|
||||||
|
name: vm_uuid
|
||||||
|
type: string
|
||||||
|
- description: VM name
|
||||||
|
in: query
|
||||||
|
name: name
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- text/html
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: HTML page
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"400":
|
||||||
|
description: Missing identifier
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
summary: Trace VM history
|
||||||
|
tags:
|
||||||
|
- vm
|
||||||
swagger: "2.0"
|
swagger: "2.0"
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver st
|
|||||||
mux.HandleFunc("/api/snapshots/hourly/force", h.SnapshotForceHourly)
|
mux.HandleFunc("/api/snapshots/hourly/force", h.SnapshotForceHourly)
|
||||||
mux.HandleFunc("/api/snapshots/migrate", h.SnapshotMigrate)
|
mux.HandleFunc("/api/snapshots/migrate", h.SnapshotMigrate)
|
||||||
mux.HandleFunc("/api/snapshots/regenerate-hourly-reports", h.SnapshotRegenerateHourlyReports)
|
mux.HandleFunc("/api/snapshots/regenerate-hourly-reports", h.SnapshotRegenerateHourlyReports)
|
||||||
|
mux.HandleFunc("/vm/trace", h.VmTrace)
|
||||||
mux.HandleFunc("/vcenters", h.VcenterList)
|
mux.HandleFunc("/vcenters", h.VcenterList)
|
||||||
mux.HandleFunc("/vcenters/totals", h.VcenterTotals)
|
mux.HandleFunc("/vcenters/totals", h.VcenterTotals)
|
||||||
mux.HandleFunc("/metrics", h.Metrics)
|
mux.HandleFunc("/metrics", h.Metrics)
|
||||||
|
|||||||
Reference in New Issue
Block a user