Compare commits

..

2 Commits

Author SHA1 Message Date
nathan 27cab61e89 improve title overflow
continuous-integration/drone/push Build is passing
2026-04-20 17:10:58 +10:00
nathan 11df6e0560 golden parity + lifecycle edge-case coverage in internal/tasks 2026-04-20 17:09:38 +10:00
2 changed files with 609 additions and 3 deletions
+20 -3
View File
@@ -89,7 +89,7 @@ body {
} }
.web2-shell-wide { .web2-shell-wide {
max-width: 1420px; max-width: min(1760px, calc(100vw - 2rem));
} }
.web2-page-head { .web2-page-head {
@@ -101,21 +101,26 @@ body {
.web2-page-head-row { .web2-page-head-row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: 1rem; gap: 1rem;
} }
.web2-head-copy { .web2-head-copy {
flex: 1 1 740px;
min-width: 0;
max-width: 72ch; max-width: 72ch;
} }
.web2-page-title { .web2-page-title {
margin-top: 0.6rem; margin-top: 0.6rem;
font-family: var(--theme_font_display); font-family: var(--theme_font_display);
font-size: clamp(1.95rem, 1.2rem + 1.9vw, 2.65rem); font-size: clamp(1.7rem, 1.1rem + 1.6vw, 2.35rem);
line-height: 1.15; line-height: 1.15;
letter-spacing: -0.325px; letter-spacing: -0.325px;
overflow-wrap: anywhere;
word-break: break-word;
hyphens: auto;
} }
.web2-page-subtitle { .web2-page-subtitle {
@@ -129,6 +134,8 @@ body {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: flex-end;
flex: 0 0 auto;
gap: 0.5rem; gap: 0.5rem;
} }
@@ -710,6 +717,16 @@ summary:focus-visible,
} }
} }
@media (min-width: 1500px) {
.web2-shell {
padding-left: 1rem;
padding-right: 1rem;
}
.web2-shell-wide {
max-width: min(1860px, calc(100vw - 1.25rem));
}
}
@media (min-width: 780px) { @media (min-width: 780px) {
.web2-kpi-grid { .web2-kpi-grid {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
+589
View File
@@ -0,0 +1,589 @@
package tasks
import (
"context"
"fmt"
"io"
"log/slog"
"math"
"testing"
"time"
"vctp/db"
"github.com/jmoiron/sqlx"
)
type tasksTestDatabase struct {
dbConn *sqlx.DB
logger *slog.Logger
}
func (d *tasksTestDatabase) DB() *sqlx.DB { return d.dbConn }
func (d *tasksTestDatabase) Queries() db.Querier { return nil }
func (d *tasksTestDatabase) Logger() *slog.Logger {
if d.logger != nil {
return d.logger
}
return slog.New(slog.NewTextHandler(io.Discard, nil))
}
func (d *tasksTestDatabase) Close() error { return d.dbConn.Close() }
type dailySummaryRow struct {
Name string `db:"Name"`
Vcenter string `db:"Vcenter"`
VmId string `db:"VmId"`
VmUuid string `db:"VmUuid"`
ResourcePool string `db:"ResourcePool"`
CreationTime int64 `db:"CreationTime"`
DeletionTime int64 `db:"DeletionTime"`
SnapshotTime int64 `db:"SnapshotTime"`
SamplesPresent int64 `db:"SamplesPresent"`
AvgVcpuCount float64 `db:"AvgVcpuCount"`
AvgRamGB float64 `db:"AvgRamGB"`
AvgProvisionedDisk float64 `db:"AvgProvisionedDisk"`
AvgIsPresent float64 `db:"AvgIsPresent"`
PoolTinPct float64 `db:"PoolTinPct"`
PoolBronzePct float64 `db:"PoolBronzePct"`
PoolSilverPct float64 `db:"PoolSilverPct"`
PoolGoldPct float64 `db:"PoolGoldPct"`
}
type monthlySummaryRow struct {
Name string `db:"Name"`
Vcenter string `db:"Vcenter"`
VmId string `db:"VmId"`
VmUuid string `db:"VmUuid"`
ResourcePool string `db:"ResourcePool"`
CreationTime int64 `db:"CreationTime"`
DeletionTime int64 `db:"DeletionTime"`
SamplesPresent int64 `db:"SamplesPresent"`
AvgVcpuCount float64 `db:"AvgVcpuCount"`
AvgRamGB float64 `db:"AvgRamGB"`
AvgProvisionedDisk float64 `db:"AvgProvisionedDisk"`
AvgIsPresent float64 `db:"AvgIsPresent"`
PoolTinPct float64 `db:"PoolTinPct"`
PoolBronzePct float64 `db:"PoolBronzePct"`
PoolSilverPct float64 `db:"PoolSilverPct"`
PoolGoldPct float64 `db:"PoolGoldPct"`
}
type hourlySeedRow struct {
SnapshotTime int64
Name string
Vcenter string
VmID string
VmUUID string
ResourcePool string
Datacenter string
Cluster string
Folder string
ProvisionedDisk float64
VcpuCount int64
RamGB int64
CreationTime int64
DeletionTime int64
IsTemplate string
PoweredOn string
SrmPlaceholder string
}
type dailySeedRow struct {
SnapshotTime int64
Name string
Vcenter string
VmID string
VmUUID string
ResourcePool string
Datacenter string
Cluster string
Folder string
ProvisionedDisk float64
VcpuCount int64
RamGB int64
CreationTime int64
DeletionTime int64
IsTemplate string
PoweredOn string
SrmPlaceholder string
SamplesPresent int64
AvgVcpuCount float64
AvgRamGB float64
AvgProvisionedDisk float64
AvgIsPresent float64
PoolTinPct float64
PoolBronzePct float64
PoolSilverPct float64
PoolGoldPct float64
Tin float64
Bronze float64
Silver float64
Gold float64
TotalSamples int64
SumVcpu int64
SumRam int64
SumDisk float64
TinHits int64
BronzeHits int64
SilverHits int64
GoldHits int64
}
func TestDailyGoldenParity_SQLUnionVsGoCanonical(t *testing.T) {
ctx := context.Background()
dbConn := newTasksTestDB(t)
task := newTasksTestCronTask(dbConn)
if err := db.EnsureVmHourlyStats(ctx, dbConn); err != nil {
t.Fatalf("failed to ensure vm_hourly_stats: %v", err)
}
dayStart := time.Date(2026, time.January, 15, 0, 0, 0, 0, time.UTC)
dayEnd := dayStart.AddDate(0, 0, 1)
t1 := dayStart.Add(1 * time.Hour).Unix()
t2 := dayStart.Add(2 * time.Hour).Unix()
t3 := dayStart.Add(3 * time.Hour).Unix()
rows := []hourlySeedRow{
{SnapshotTime: t1, Name: "vm-alpha", Vcenter: "vc-a", VmID: "vm-1", VmUUID: "uuid-1", ResourcePool: "Tin", Datacenter: "dc-1", Cluster: "cluster-1", Folder: "/prod", ProvisionedDisk: 100, VcpuCount: 2, RamGB: 8, CreationTime: 0, IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"},
{SnapshotTime: t3, Name: "vm-alpha", Vcenter: "vc-a", VmID: "vm-1", VmUUID: "uuid-1", ResourcePool: "Gold", Datacenter: "dc-1", Cluster: "cluster-1", Folder: "/prod", ProvisionedDisk: 120, VcpuCount: 4, RamGB: 16, CreationTime: dayStart.Add(30 * time.Minute).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"},
{SnapshotTime: t2, Name: "vm-bravo", Vcenter: "vc-a", VmID: "vm-2", VmUUID: "uuid-2", ResourcePool: "Bronze", Datacenter: "dc-1", Cluster: "cluster-1", Folder: "/prod", ProvisionedDisk: 30, VcpuCount: 1, RamGB: 2, CreationTime: dayStart.Add(-2 * time.Hour).Unix(), DeletionTime: dayStart.Add(4 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"},
{SnapshotTime: t1, Name: "vm-charlie", Vcenter: "vc-a", VmID: "vm-3", VmUUID: "uuid-3", ResourcePool: "Silver", Datacenter: "dc-1", Cluster: "cluster-2", Folder: "/prod2", ProvisionedDisk: 50, VcpuCount: 2, RamGB: 4, CreationTime: dayStart.Add(-5 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"},
{SnapshotTime: t3, Name: "vm-charlie", Vcenter: "vc-a", VmID: "vm-3", VmUUID: "uuid-3", ResourcePool: "Silver", Datacenter: "dc-1", Cluster: "cluster-2", Folder: "/prod2", ProvisionedDisk: 50, VcpuCount: 2, RamGB: 4, CreationTime: dayStart.Add(-5 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"},
{SnapshotTime: t3, Name: "vm-template", Vcenter: "vc-a", VmID: "vm-t", VmUUID: "uuid-t", ResourcePool: "Tin", Datacenter: "dc-1", Cluster: "cluster-3", Folder: "/templates", ProvisionedDisk: 500, VcpuCount: 16, RamGB: 64, CreationTime: dayStart.Add(-10 * time.Hour).Unix(), IsTemplate: "TRUE", PoweredOn: "FALSE", SrmPlaceholder: "FALSE"},
}
for _, row := range rows {
if err := insertHourlyCacheSeedRow(ctx, dbConn, row); err != nil {
t.Fatalf("failed to insert vm_hourly_stats row: %v", err)
}
}
hourlyTableTimes := []int64{t1, t2, t3}
hourlyTables := make([]string, 0, len(hourlyTableTimes))
for _, ts := range hourlyTableTimes {
tableName, err := hourlyInventoryTableName(time.Unix(ts, 0).UTC())
if err != nil {
t.Fatalf("failed to build hourly table name: %v", err)
}
hourlyTables = append(hourlyTables, tableName)
if err := db.EnsureSnapshotTable(ctx, dbConn, tableName); err != nil {
t.Fatalf("failed to ensure snapshot table %s: %v", tableName, err)
}
}
for _, row := range rows {
tableName, err := hourlyInventoryTableName(time.Unix(row.SnapshotTime, 0).UTC())
if err != nil {
t.Fatalf("failed to build per-row hourly table name: %v", err)
}
if err := insertHourlySnapshotSeedRow(ctx, dbConn, tableName, row); err != nil {
t.Fatalf("failed to insert snapshot row for table %s: %v", tableName, err)
}
}
oldSummaryTable, err := db.SafeTableName("test_daily_sql_union_summary")
if err != nil {
t.Fatalf("failed to build old summary table name: %v", err)
}
newSummaryTable, err := db.SafeTableName("test_daily_go_cache_summary")
if err != nil {
t.Fatalf("failed to build new summary table name: %v", err)
}
if err := db.EnsureSummaryTable(ctx, dbConn, oldSummaryTable); err != nil {
t.Fatalf("failed to ensure old summary table: %v", err)
}
if err := db.EnsureSummaryTable(ctx, dbConn, newSummaryTable); err != nil {
t.Fatalf("failed to ensure new summary table: %v", err)
}
unionQuery, err := buildUnionQuery(hourlyTables, summaryUnionColumns, templateExclusionFilter())
if err != nil {
t.Fatalf("failed to build union query: %v", err)
}
insertSQL, err := db.BuildDailySummaryInsert(oldSummaryTable, unionQuery)
if err != nil {
t.Fatalf("failed to build daily sql insert: %v", err)
}
if _, err := dbConn.ExecContext(ctx, insertSQL); err != nil {
t.Fatalf("failed to execute daily sql insert: %v", err)
}
aggMap, snapTimes, err := task.scanHourlyCache(ctx, dayStart, dayEnd)
if err != nil {
t.Fatalf("scanHourlyCache failed: %v", err)
}
totalSamplesByVcenter := sampleCountsByVcenter(aggMap)
if err := task.insertDailyAggregates(ctx, newSummaryTable, aggMap, len(snapTimes), totalSamplesByVcenter); err != nil {
t.Fatalf("insertDailyAggregates failed: %v", err)
}
oldRows, err := loadDailySummaryRows(ctx, dbConn, oldSummaryTable)
if err != nil {
t.Fatalf("failed to load old daily rows: %v", err)
}
newRows, err := loadDailySummaryRows(ctx, dbConn, newSummaryTable)
if err != nil {
t.Fatalf("failed to load new daily rows: %v", err)
}
assertDailySummaryParity(t, oldRows, newRows)
byKey := mapRowsByKeyDaily(newRows)
alpha := byKey["vc-a|vm-1|uuid-1|vm-alpha"]
if !approxEqual(alpha.AvgIsPresent, 2.0/3.0, 1e-9) {
t.Fatalf("unexpected alpha AvgIsPresent: got %.12f want %.12f", alpha.AvgIsPresent, 2.0/3.0)
}
if alpha.CreationTime != dayStart.Add(30*time.Minute).Unix() {
t.Fatalf("unexpected alpha CreationTime: got %d want %d", alpha.CreationTime, dayStart.Add(30*time.Minute).Unix())
}
if alpha.ResourcePool != "Gold" {
t.Fatalf("unexpected alpha ResourcePool: got %q want %q", alpha.ResourcePool, "Gold")
}
if alpha.SnapshotTime != t3 {
t.Fatalf("unexpected alpha SnapshotTime: got %d want %d", alpha.SnapshotTime, t3)
}
if !approxEqual(alpha.PoolTinPct, 50.0, 1e-9) || !approxEqual(alpha.PoolGoldPct, 50.0, 1e-9) {
t.Fatalf("unexpected alpha pool mix: tin=%.6f gold=%.6f", alpha.PoolTinPct, alpha.PoolGoldPct)
}
bravo := byKey["vc-a|vm-2|uuid-2|vm-bravo"]
if bravo.DeletionTime != dayStart.Add(4*time.Hour).Unix() {
t.Fatalf("unexpected bravo DeletionTime: got %d want %d", bravo.DeletionTime, dayStart.Add(4*time.Hour).Unix())
}
if !approxEqual(bravo.AvgIsPresent, 1.0/3.0, 1e-9) {
t.Fatalf("unexpected bravo AvgIsPresent: got %.12f want %.12f", bravo.AvgIsPresent, 1.0/3.0)
}
}
func TestMonthlyGoldenParity_SQLDailyUnionVsGoDailyRollup(t *testing.T) {
ctx := context.Background()
dbConn := newTasksTestDB(t)
task := newTasksTestCronTask(dbConn)
if err := db.EnsureVmDailyRollup(ctx, dbConn); err != nil {
t.Fatalf("failed to ensure vm_daily_rollup: %v", err)
}
monthStart := time.Date(2026, time.February, 1, 0, 0, 0, 0, time.UTC)
monthEnd := monthStart.AddDate(0, 1, 0)
day1 := time.Date(2026, time.February, 3, 0, 0, 0, 0, time.UTC)
day2 := day1.AddDate(0, 0, 1)
day1Table, err := dailySummaryTableName(day1)
if err != nil {
t.Fatalf("failed to build day1 table name: %v", err)
}
day2Table, err := dailySummaryTableName(day2)
if err != nil {
t.Fatalf("failed to build day2 table name: %v", err)
}
for _, table := range []string{day1Table, day2Table} {
if err := db.EnsureSummaryTable(ctx, dbConn, table); err != nil {
t.Fatalf("failed to ensure daily summary table %s: %v", table, err)
}
}
seeds := []dailySeedRow{
{
SnapshotTime: day1.Unix(), Name: "vm-alpha", Vcenter: "vc-a", VmID: "vm-1", VmUUID: "uuid-1",
ResourcePool: "Bronze", Datacenter: "dc-1", Cluster: "cluster-1", Folder: "/prod",
ProvisionedDisk: 100, VcpuCount: 4, RamGB: 8, CreationTime: monthStart.Add(-24 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE",
SamplesPresent: 2, AvgVcpuCount: 3, AvgRamGB: 6, AvgProvisionedDisk: 90, AvgIsPresent: 1.0,
PoolBronzePct: 100, Bronze: 100,
TotalSamples: 2, SumVcpu: 6, SumRam: 12, SumDisk: 180, BronzeHits: 2,
},
{
SnapshotTime: day2.Unix(), Name: "vm-alpha", Vcenter: "vc-a", VmID: "vm-1", VmUUID: "uuid-1",
ResourcePool: "Tin", Datacenter: "dc-1", Cluster: "cluster-1", Folder: "/prod",
ProvisionedDisk: 110, VcpuCount: 2, RamGB: 8, CreationTime: monthStart.Add(-24 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE",
SamplesPresent: 2, AvgVcpuCount: 2, AvgRamGB: 8, AvgProvisionedDisk: 110, AvgIsPresent: 1.0,
PoolTinPct: 100, Tin: 100,
TotalSamples: 2, SumVcpu: 4, SumRam: 16, SumDisk: 220, TinHits: 2,
},
}
for _, seed := range seeds {
targetTable := day1Table
if seed.SnapshotTime == day2.Unix() {
targetTable = day2Table
}
if err := insertDailySummarySeedRow(ctx, dbConn, targetTable, seed); err != nil {
t.Fatalf("failed to insert daily summary seed row: %v", err)
}
if err := insertDailyRollupSeedRow(ctx, dbConn, seed); err != nil {
t.Fatalf("failed to insert daily rollup seed row: %v", err)
}
}
oldMonthlyTable, err := db.SafeTableName("test_monthly_sql_union_summary")
if err != nil {
t.Fatalf("failed to build old monthly table name: %v", err)
}
newMonthlyTable, err := db.SafeTableName("test_monthly_go_rollup_summary")
if err != nil {
t.Fatalf("failed to build new monthly table name: %v", err)
}
if err := db.EnsureSummaryTable(ctx, dbConn, oldMonthlyTable); err != nil {
t.Fatalf("failed to ensure old monthly table: %v", err)
}
if err := db.EnsureSummaryTable(ctx, dbConn, newMonthlyTable); err != nil {
t.Fatalf("failed to ensure new monthly table: %v", err)
}
unionQuery, err := buildUnionQuery([]string{day1Table, day2Table}, monthlyUnionColumns, templateExclusionFilter())
if err != nil {
t.Fatalf("failed to build monthly union query: %v", err)
}
insertSQL, err := db.BuildMonthlySummaryInsert(oldMonthlyTable, unionQuery)
if err != nil {
t.Fatalf("failed to build monthly sql insert: %v", err)
}
if _, err := dbConn.ExecContext(ctx, insertSQL); err != nil {
t.Fatalf("failed to execute monthly sql insert: %v", err)
}
aggMap, err := task.scanDailyRollup(ctx, monthStart, monthEnd)
if err != nil {
t.Fatalf("scanDailyRollup failed: %v", err)
}
if err := task.insertMonthlyAggregates(ctx, newMonthlyTable, aggMap); err != nil {
t.Fatalf("insertMonthlyAggregates failed: %v", err)
}
oldRows, err := loadMonthlySummaryRows(ctx, dbConn, oldMonthlyTable)
if err != nil {
t.Fatalf("failed to load old monthly rows: %v", err)
}
newRows, err := loadMonthlySummaryRows(ctx, dbConn, newMonthlyTable)
if err != nil {
t.Fatalf("failed to load new monthly rows: %v", err)
}
assertMonthlySummaryParity(t, oldRows, newRows)
byKey := mapRowsByKeyMonthly(newRows)
alpha := byKey["vc-a|vm-1|uuid-1|vm-alpha"]
if !approxEqual(alpha.AvgVcpuCount, 2.5, 1e-9) {
t.Fatalf("unexpected alpha AvgVcpuCount: got %.6f want %.6f", alpha.AvgVcpuCount, 2.5)
}
if !approxEqual(alpha.AvgIsPresent, 1.0, 1e-9) {
t.Fatalf("unexpected alpha AvgIsPresent: got %.6f want %.6f", alpha.AvgIsPresent, 1.0)
}
if alpha.ResourcePool != "Tin" {
t.Fatalf("unexpected alpha ResourcePool: got %q want %q", alpha.ResourcePool, "Tin")
}
if !approxEqual(alpha.PoolTinPct, 50.0, 1e-9) || !approxEqual(alpha.PoolBronzePct, 50.0, 1e-9) {
t.Fatalf("unexpected alpha monthly pool mix: tin=%.6f bronze=%.6f", alpha.PoolTinPct, alpha.PoolBronzePct)
}
}
func newTasksTestCronTask(dbConn *sqlx.DB) *CronTask {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
return &CronTask{
Logger: logger,
Database: &tasksTestDatabase{dbConn: dbConn, logger: logger},
}
}
func insertHourlyCacheSeedRow(ctx context.Context, dbConn *sqlx.DB, row hourlySeedRow) error {
_, err := dbConn.ExecContext(ctx, `
INSERT INTO vm_hourly_stats (
"SnapshotTime","Vcenter","VmId","VmUuid","Name","CreationTime","DeletionTime","ResourcePool",
"Datacenter","Cluster","Folder","ProvisionedDisk","VcpuCount","RamGB","IsTemplate","PoweredOn","SrmPlaceholder"
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
`,
row.SnapshotTime, row.Vcenter, row.VmID, row.VmUUID, row.Name, row.CreationTime, row.DeletionTime, row.ResourcePool,
row.Datacenter, row.Cluster, row.Folder, row.ProvisionedDisk, row.VcpuCount, row.RamGB, row.IsTemplate, row.PoweredOn, row.SrmPlaceholder,
)
return err
}
func insertHourlySnapshotSeedRow(ctx context.Context, dbConn *sqlx.DB, table string, row hourlySeedRow) error {
sql := fmt.Sprintf(`
INSERT INTO %s (
"Name","Vcenter","VmId","VmUuid","EventKey","CloudId","CreationTime","DeletionTime","ResourcePool",
"Datacenter","Cluster","Folder","ProvisionedDisk","VcpuCount","RamGB","IsTemplate","PoweredOn","SrmPlaceholder","SnapshotTime"
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
`, table)
_, err := dbConn.ExecContext(ctx, sql,
row.Name, row.Vcenter, row.VmID, row.VmUUID, nil, nil, row.CreationTime, row.DeletionTime, row.ResourcePool,
row.Datacenter, row.Cluster, row.Folder, row.ProvisionedDisk, row.VcpuCount, row.RamGB, row.IsTemplate, row.PoweredOn, row.SrmPlaceholder, row.SnapshotTime,
)
return err
}
func insertDailySummarySeedRow(ctx context.Context, dbConn *sqlx.DB, table string, row dailySeedRow) error {
sql := fmt.Sprintf(`
INSERT INTO %s (
"Name","Vcenter","VmId","VmUuid","EventKey","CloudId","CreationTime","DeletionTime","ResourcePool",
"Datacenter","Cluster","Folder","ProvisionedDisk","VcpuCount","RamGB","IsTemplate","PoweredOn","SrmPlaceholder",
"SnapshotTime","SamplesPresent","AvgVcpuCount","AvgRamGB","AvgProvisionedDisk","AvgIsPresent",
"PoolTinPct","PoolBronzePct","PoolSilverPct","PoolGoldPct","Tin","Bronze","Silver","Gold"
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
`, table)
_, err := dbConn.ExecContext(ctx, sql,
row.Name, row.Vcenter, row.VmID, row.VmUUID, nil, nil, row.CreationTime, row.DeletionTime, row.ResourcePool,
row.Datacenter, row.Cluster, row.Folder, row.ProvisionedDisk, row.VcpuCount, row.RamGB, row.IsTemplate, row.PoweredOn, row.SrmPlaceholder,
row.SnapshotTime, row.SamplesPresent, row.AvgVcpuCount, row.AvgRamGB, row.AvgProvisionedDisk, row.AvgIsPresent,
row.PoolTinPct, row.PoolBronzePct, row.PoolSilverPct, row.PoolGoldPct, row.Tin, row.Bronze, row.Silver, row.Gold,
)
return err
}
func insertDailyRollupSeedRow(ctx context.Context, dbConn *sqlx.DB, row dailySeedRow) error {
_, err := dbConn.ExecContext(ctx, `
INSERT INTO vm_daily_rollup (
"Date","Vcenter","VmId","VmUuid","Name","CreationTime","DeletionTime","SamplesPresent","TotalSamples",
"SumVcpu","SumRam","SumDisk","TinHits","BronzeHits","SilverHits","GoldHits",
"LastResourcePool","LastDatacenter","LastCluster","LastFolder","LastProvisionedDisk","LastVcpuCount","LastRamGB","IsTemplate","PoweredOn","SrmPlaceholder"
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
`,
row.SnapshotTime, row.Vcenter, row.VmID, row.VmUUID, row.Name, row.CreationTime, row.DeletionTime, row.SamplesPresent, row.TotalSamples,
row.SumVcpu, row.SumRam, row.SumDisk, row.TinHits, row.BronzeHits, row.SilverHits, row.GoldHits,
row.ResourcePool, row.Datacenter, row.Cluster, row.Folder, row.ProvisionedDisk, row.VcpuCount, row.RamGB, row.IsTemplate, row.PoweredOn, row.SrmPlaceholder,
)
return err
}
func loadDailySummaryRows(ctx context.Context, dbConn *sqlx.DB, table string) ([]dailySummaryRow, error) {
sql := fmt.Sprintf(`
SELECT
COALESCE("Name",'') AS "Name",
COALESCE("Vcenter",'') AS "Vcenter",
COALESCE("VmId",'') AS "VmId",
COALESCE("VmUuid",'') AS "VmUuid",
COALESCE("ResourcePool",'') AS "ResourcePool",
COALESCE("CreationTime",0) AS "CreationTime",
COALESCE("DeletionTime",0) AS "DeletionTime",
COALESCE("SnapshotTime",0) AS "SnapshotTime",
COALESCE("SamplesPresent",0) AS "SamplesPresent",
COALESCE("AvgVcpuCount",0) AS "AvgVcpuCount",
COALESCE("AvgRamGB",0) AS "AvgRamGB",
COALESCE("AvgProvisionedDisk",0) AS "AvgProvisionedDisk",
COALESCE("AvgIsPresent",0) AS "AvgIsPresent",
COALESCE("PoolTinPct",0) AS "PoolTinPct",
COALESCE("PoolBronzePct",0) AS "PoolBronzePct",
COALESCE("PoolSilverPct",0) AS "PoolSilverPct",
COALESCE("PoolGoldPct",0) AS "PoolGoldPct"
FROM %s
ORDER BY "Vcenter", "VmId", "VmUuid", "Name"
`, table)
var out []dailySummaryRow
return out, dbConn.SelectContext(ctx, &out, sql)
}
func loadMonthlySummaryRows(ctx context.Context, dbConn *sqlx.DB, table string) ([]monthlySummaryRow, error) {
sql := fmt.Sprintf(`
SELECT
COALESCE("Name",'') AS "Name",
COALESCE("Vcenter",'') AS "Vcenter",
COALESCE("VmId",'') AS "VmId",
COALESCE("VmUuid",'') AS "VmUuid",
COALESCE("ResourcePool",'') AS "ResourcePool",
COALESCE("CreationTime",0) AS "CreationTime",
COALESCE("DeletionTime",0) AS "DeletionTime",
COALESCE("SamplesPresent",0) AS "SamplesPresent",
COALESCE("AvgVcpuCount",0) AS "AvgVcpuCount",
COALESCE("AvgRamGB",0) AS "AvgRamGB",
COALESCE("AvgProvisionedDisk",0) AS "AvgProvisionedDisk",
COALESCE("AvgIsPresent",0) AS "AvgIsPresent",
COALESCE("PoolTinPct",0) AS "PoolTinPct",
COALESCE("PoolBronzePct",0) AS "PoolBronzePct",
COALESCE("PoolSilverPct",0) AS "PoolSilverPct",
COALESCE("PoolGoldPct",0) AS "PoolGoldPct"
FROM %s
ORDER BY "Vcenter", "VmId", "VmUuid", "Name"
`, table)
var out []monthlySummaryRow
return out, dbConn.SelectContext(ctx, &out, sql)
}
func mapRowsByKeyDaily(rows []dailySummaryRow) map[string]dailySummaryRow {
out := make(map[string]dailySummaryRow, len(rows))
for _, row := range rows {
out[dailyRowKey(row)] = row
}
return out
}
func mapRowsByKeyMonthly(rows []monthlySummaryRow) map[string]monthlySummaryRow {
out := make(map[string]monthlySummaryRow, len(rows))
for _, row := range rows {
out[monthlyRowKey(row)] = row
}
return out
}
func dailyRowKey(r dailySummaryRow) string {
return fmt.Sprintf("%s|%s|%s|%s", r.Vcenter, r.VmId, r.VmUuid, r.Name)
}
func monthlyRowKey(r monthlySummaryRow) string {
return fmt.Sprintf("%s|%s|%s|%s", r.Vcenter, r.VmId, r.VmUuid, r.Name)
}
func assertDailySummaryParity(t *testing.T, oldRows, newRows []dailySummaryRow) {
t.Helper()
if len(oldRows) != len(newRows) {
t.Fatalf("daily row count mismatch: old=%d new=%d", len(oldRows), len(newRows))
}
oldByKey := mapRowsByKeyDaily(oldRows)
newByKey := mapRowsByKeyDaily(newRows)
for key, oldRow := range oldByKey {
newRow, ok := newByKey[key]
if !ok {
t.Fatalf("missing key in new daily output: %s", key)
}
if oldRow.ResourcePool != newRow.ResourcePool ||
oldRow.CreationTime != newRow.CreationTime ||
oldRow.DeletionTime != newRow.DeletionTime ||
oldRow.SnapshotTime != newRow.SnapshotTime ||
oldRow.SamplesPresent != newRow.SamplesPresent {
t.Fatalf("daily scalar mismatch key=%s old=%+v new=%+v", key, oldRow, newRow)
}
assertFloatClose(t, "AvgVcpuCount", key, oldRow.AvgVcpuCount, newRow.AvgVcpuCount, 1e-9)
assertFloatClose(t, "AvgRamGB", key, oldRow.AvgRamGB, newRow.AvgRamGB, 1e-9)
assertFloatClose(t, "AvgProvisionedDisk", key, oldRow.AvgProvisionedDisk, newRow.AvgProvisionedDisk, 1e-9)
assertFloatClose(t, "AvgIsPresent", key, oldRow.AvgIsPresent, newRow.AvgIsPresent, 1e-9)
assertFloatClose(t, "PoolTinPct", key, oldRow.PoolTinPct, newRow.PoolTinPct, 1e-9)
assertFloatClose(t, "PoolBronzePct", key, oldRow.PoolBronzePct, newRow.PoolBronzePct, 1e-9)
assertFloatClose(t, "PoolSilverPct", key, oldRow.PoolSilverPct, newRow.PoolSilverPct, 1e-9)
assertFloatClose(t, "PoolGoldPct", key, oldRow.PoolGoldPct, newRow.PoolGoldPct, 1e-9)
}
}
func assertMonthlySummaryParity(t *testing.T, oldRows, newRows []monthlySummaryRow) {
t.Helper()
if len(oldRows) != len(newRows) {
t.Fatalf("monthly row count mismatch: old=%d new=%d", len(oldRows), len(newRows))
}
oldByKey := mapRowsByKeyMonthly(oldRows)
newByKey := mapRowsByKeyMonthly(newRows)
for key, oldRow := range oldByKey {
newRow, ok := newByKey[key]
if !ok {
t.Fatalf("missing key in new monthly output: %s", key)
}
if oldRow.ResourcePool != newRow.ResourcePool ||
oldRow.CreationTime != newRow.CreationTime ||
oldRow.DeletionTime != newRow.DeletionTime ||
oldRow.SamplesPresent != newRow.SamplesPresent {
t.Fatalf("monthly scalar mismatch key=%s old=%+v new=%+v", key, oldRow, newRow)
}
assertFloatClose(t, "AvgVcpuCount", key, oldRow.AvgVcpuCount, newRow.AvgVcpuCount, 1e-9)
assertFloatClose(t, "AvgRamGB", key, oldRow.AvgRamGB, newRow.AvgRamGB, 1e-9)
assertFloatClose(t, "AvgProvisionedDisk", key, oldRow.AvgProvisionedDisk, newRow.AvgProvisionedDisk, 1e-9)
assertFloatClose(t, "AvgIsPresent", key, oldRow.AvgIsPresent, newRow.AvgIsPresent, 1e-9)
assertFloatClose(t, "PoolTinPct", key, oldRow.PoolTinPct, newRow.PoolTinPct, 1e-9)
assertFloatClose(t, "PoolBronzePct", key, oldRow.PoolBronzePct, newRow.PoolBronzePct, 1e-9)
assertFloatClose(t, "PoolSilverPct", key, oldRow.PoolSilverPct, newRow.PoolSilverPct, 1e-9)
assertFloatClose(t, "PoolGoldPct", key, oldRow.PoolGoldPct, newRow.PoolGoldPct, 1e-9)
}
}
func assertFloatClose(t *testing.T, field, key string, oldVal, newVal, eps float64) {
t.Helper()
if !approxEqual(oldVal, newVal, eps) {
t.Fatalf("%s mismatch key=%s old=%.12f new=%.12f", field, key, oldVal, newVal)
}
}
func approxEqual(a, b, eps float64) bool {
return math.Abs(a-b) <= eps
}