add vcenter totals line graph
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:
382
db/helpers.go
382
db/helpers.go
@@ -354,6 +354,388 @@ func ApplySQLiteTuning(ctx context.Context, dbConn *sqlx.DB) {
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureVmIdentityTables creates the identity and rename audit tables.
|
||||
func EnsureVmIdentityTables(ctx context.Context, dbConn *sqlx.DB) error {
|
||||
driver := strings.ToLower(dbConn.DriverName())
|
||||
var identityDDL, renameDDL string
|
||||
switch driver {
|
||||
case "pgx", "postgres":
|
||||
identityDDL = `
|
||||
CREATE TABLE IF NOT EXISTS vm_identity (
|
||||
"VmId" TEXT NOT NULL,
|
||||
"VmUuid" TEXT NOT NULL,
|
||||
"Vcenter" TEXT NOT NULL,
|
||||
"Name" TEXT NOT NULL,
|
||||
"Cluster" TEXT,
|
||||
"FirstSeen" BIGINT NOT NULL,
|
||||
"LastSeen" BIGINT NOT NULL,
|
||||
PRIMARY KEY ("VmId","VmUuid","Vcenter")
|
||||
)`
|
||||
renameDDL = `
|
||||
CREATE TABLE IF NOT EXISTS vm_renames (
|
||||
"RowId" BIGSERIAL PRIMARY KEY,
|
||||
"VmId" TEXT NOT NULL,
|
||||
"VmUuid" TEXT NOT NULL,
|
||||
"Vcenter" TEXT NOT NULL,
|
||||
"OldName" TEXT,
|
||||
"NewName" TEXT,
|
||||
"OldCluster" TEXT,
|
||||
"NewCluster" TEXT,
|
||||
"SnapshotTime" BIGINT NOT NULL
|
||||
)`
|
||||
default:
|
||||
identityDDL = `
|
||||
CREATE TABLE IF NOT EXISTS vm_identity (
|
||||
"VmId" TEXT NOT NULL,
|
||||
"VmUuid" TEXT NOT NULL,
|
||||
"Vcenter" TEXT NOT NULL,
|
||||
"Name" TEXT NOT NULL,
|
||||
"Cluster" TEXT,
|
||||
"FirstSeen" BIGINT NOT NULL,
|
||||
"LastSeen" BIGINT NOT NULL,
|
||||
PRIMARY KEY ("VmId","VmUuid","Vcenter")
|
||||
)`
|
||||
renameDDL = `
|
||||
CREATE TABLE IF NOT EXISTS vm_renames (
|
||||
"RowId" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"VmId" TEXT NOT NULL,
|
||||
"VmUuid" TEXT NOT NULL,
|
||||
"Vcenter" TEXT NOT NULL,
|
||||
"OldName" TEXT,
|
||||
"NewName" TEXT,
|
||||
"OldCluster" TEXT,
|
||||
"NewCluster" TEXT,
|
||||
"SnapshotTime" BIGINT NOT NULL
|
||||
)`
|
||||
}
|
||||
if _, err := dbConn.ExecContext(ctx, identityDDL); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := dbConn.ExecContext(ctx, renameDDL); err != nil {
|
||||
return err
|
||||
}
|
||||
indexes := []string{
|
||||
`CREATE INDEX IF NOT EXISTS vm_identity_vcenter_idx ON vm_identity ("Vcenter")`,
|
||||
`CREATE INDEX IF NOT EXISTS vm_identity_uuid_idx ON vm_identity ("VmUuid","Vcenter")`,
|
||||
`CREATE INDEX IF NOT EXISTS vm_identity_name_idx ON vm_identity ("Name","Vcenter")`,
|
||||
`CREATE INDEX IF NOT EXISTS vm_renames_vcenter_idx ON vm_renames ("Vcenter","SnapshotTime")`,
|
||||
}
|
||||
for _, idx := range indexes {
|
||||
if _, err := dbConn.ExecContext(ctx, idx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpsertVmIdentity updates/creates the identity record and records rename events.
|
||||
func UpsertVmIdentity(ctx context.Context, dbConn *sqlx.DB, vcenter string, vmId, vmUuid sql.NullString, name string, cluster sql.NullString, snapshotTime time.Time) error {
|
||||
keyVmID := strings.TrimSpace(vmId.String)
|
||||
keyUuid := strings.TrimSpace(vmUuid.String)
|
||||
if keyVmID == "" || keyUuid == "" || strings.TrimSpace(vcenter) == "" {
|
||||
return nil
|
||||
}
|
||||
if err := EnsureVmIdentityTables(ctx, dbConn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type identityRow struct {
|
||||
Name string `db:"Name"`
|
||||
Cluster sql.NullString `db:"Cluster"`
|
||||
FirstSeen sql.NullInt64 `db:"FirstSeen"`
|
||||
LastSeen sql.NullInt64 `db:"LastSeen"`
|
||||
}
|
||||
var existing identityRow
|
||||
err := dbConn.GetContext(ctx, &existing, `
|
||||
SELECT "Name","Cluster","FirstSeen","LastSeen"
|
||||
FROM vm_identity
|
||||
WHERE "Vcenter" = $1 AND "VmId" = $2 AND "VmUuid" = $3
|
||||
`, vcenter, keyVmID, keyUuid)
|
||||
|
||||
if err != nil {
|
||||
if strings.Contains(strings.ToLower(err.Error()), "no rows") {
|
||||
_, err = dbConn.ExecContext(ctx, `
|
||||
INSERT INTO vm_identity ("VmId","VmUuid","Vcenter","Name","Cluster","FirstSeen","LastSeen")
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$6)
|
||||
`, keyVmID, keyUuid, vcenter, name, nullString(cluster), snapshotTime.Unix())
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
renamed := !strings.EqualFold(existing.Name, name) || !strings.EqualFold(strings.TrimSpace(existing.Cluster.String), strings.TrimSpace(cluster.String))
|
||||
if renamed {
|
||||
_, _ = dbConn.ExecContext(ctx, `
|
||||
INSERT INTO vm_renames ("VmId","VmUuid","Vcenter","OldName","NewName","OldCluster","NewCluster","SnapshotTime")
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
|
||||
`, keyVmID, keyUuid, vcenter, existing.Name, name, existing.Cluster.String, cluster.String, snapshotTime.Unix())
|
||||
}
|
||||
_, err = dbConn.ExecContext(ctx, `
|
||||
UPDATE vm_identity
|
||||
SET "Name" = $1, "Cluster" = $2, "LastSeen" = $3
|
||||
WHERE "Vcenter" = $4 AND "VmId" = $5 AND "VmUuid" = $6
|
||||
`, name, nullString(cluster), snapshotTime.Unix(), vcenter, keyVmID, keyUuid)
|
||||
return err
|
||||
}
|
||||
|
||||
func nullString(val sql.NullString) interface{} {
|
||||
if val.Valid {
|
||||
return val.String
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureVcenterTotalsTable creates the vcenter_totals table if missing.
|
||||
func EnsureVcenterTotalsTable(ctx context.Context, dbConn *sqlx.DB) error {
|
||||
driver := strings.ToLower(dbConn.DriverName())
|
||||
var ddl string
|
||||
switch driver {
|
||||
case "pgx", "postgres":
|
||||
ddl = `
|
||||
CREATE TABLE IF NOT EXISTS vcenter_totals (
|
||||
"RowId" BIGSERIAL PRIMARY KEY,
|
||||
"Vcenter" TEXT NOT NULL,
|
||||
"SnapshotTime" BIGINT NOT NULL,
|
||||
"VmCount" BIGINT NOT NULL,
|
||||
"VcpuTotal" BIGINT NOT NULL,
|
||||
"RamTotalGB" BIGINT NOT NULL
|
||||
);`
|
||||
default:
|
||||
ddl = `
|
||||
CREATE TABLE IF NOT EXISTS vcenter_totals (
|
||||
"RowId" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"Vcenter" TEXT NOT NULL,
|
||||
"SnapshotTime" BIGINT NOT NULL,
|
||||
"VmCount" BIGINT NOT NULL,
|
||||
"VcpuTotal" BIGINT NOT NULL,
|
||||
"RamTotalGB" BIGINT NOT NULL
|
||||
);`
|
||||
}
|
||||
if _, err := dbConn.ExecContext(ctx, ddl); err != nil {
|
||||
return err
|
||||
}
|
||||
indexes := []string{
|
||||
`CREATE INDEX IF NOT EXISTS vcenter_totals_vc_time_idx ON vcenter_totals ("Vcenter","SnapshotTime" DESC)`,
|
||||
}
|
||||
for _, idx := range indexes {
|
||||
if _, err := dbConn.ExecContext(ctx, idx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertVcenterTotals records totals for a vcenter at a snapshot time.
|
||||
func InsertVcenterTotals(ctx context.Context, dbConn *sqlx.DB, vcenter string, snapshotTime time.Time, vmCount, vcpuTotal, ramTotal int64) error {
|
||||
if strings.TrimSpace(vcenter) == "" {
|
||||
return fmt.Errorf("vcenter is empty")
|
||||
}
|
||||
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := dbConn.ExecContext(ctx, `
|
||||
INSERT INTO vcenter_totals ("Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB")
|
||||
VALUES ($1,$2,$3,$4,$5)
|
||||
`, vcenter, snapshotTime.Unix(), vmCount, vcpuTotal, ramTotal)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListVcenters returns distinct vcenter URLs tracked.
|
||||
func ListVcenters(ctx context.Context, dbConn *sqlx.DB) ([]string, error) {
|
||||
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := dbConn.QueryxContext(ctx, `SELECT DISTINCT "Vcenter" FROM vcenter_totals ORDER BY "Vcenter"`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []string
|
||||
for rows.Next() {
|
||||
var v string
|
||||
if err := rows.Scan(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// VcenterTotalRow holds per-snapshot totals for a vcenter.
|
||||
type VcenterTotalRow struct {
|
||||
SnapshotTime int64 `db:"SnapshotTime"`
|
||||
Vcenter string `db:"Vcenter"`
|
||||
VmCount int64 `db:"VmCount"`
|
||||
VcpuTotal int64 `db:"VcpuTotal"`
|
||||
RamTotalGB int64 `db:"RamTotalGB"`
|
||||
}
|
||||
|
||||
// ListVcenterTotals lists totals for a vcenter sorted by snapshot_time desc, limited.
|
||||
func ListVcenterTotals(ctx context.Context, dbConn *sqlx.DB, vcenter string, limit int) ([]VcenterTotalRow, error) {
|
||||
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 200
|
||||
}
|
||||
rows := make([]VcenterTotalRow, 0, limit)
|
||||
query := `
|
||||
SELECT "Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB"
|
||||
FROM vcenter_totals
|
||||
WHERE "Vcenter" = $1
|
||||
ORDER BY "SnapshotTime" DESC
|
||||
LIMIT $2`
|
||||
if err := dbConn.SelectContext(ctx, &rows, query, vcenter, limit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// ListVcenterTotalsByType returns totals for a vcenter for the requested snapshot type (hourly, daily, monthly).
|
||||
// Hourly values come from vcenter_totals; daily/monthly are derived from the summary tables referenced in snapshot_registry.
|
||||
func ListVcenterTotalsByType(ctx context.Context, dbConn *sqlx.DB, vcenter string, snapshotType string, limit int) ([]VcenterTotalRow, error) {
|
||||
snapshotType = strings.ToLower(snapshotType)
|
||||
if snapshotType == "" {
|
||||
snapshotType = "hourly"
|
||||
}
|
||||
if snapshotType == "hourly" {
|
||||
return ListVcenterTotals(ctx, dbConn, vcenter, limit)
|
||||
}
|
||||
|
||||
if limit <= 0 {
|
||||
limit = 200
|
||||
}
|
||||
|
||||
driver := strings.ToLower(dbConn.DriverName())
|
||||
query := `
|
||||
SELECT table_name, snapshot_time
|
||||
FROM snapshot_registry
|
||||
WHERE snapshot_type = $1
|
||||
ORDER BY snapshot_time DESC
|
||||
LIMIT $2
|
||||
`
|
||||
if driver == "sqlite" {
|
||||
query = strings.ReplaceAll(query, "$1", "?")
|
||||
query = strings.ReplaceAll(query, "$2", "?")
|
||||
}
|
||||
|
||||
var regRows []struct {
|
||||
TableName string `db:"table_name"`
|
||||
SnapshotTime int64 `db:"snapshot_time"`
|
||||
}
|
||||
if err := dbConn.SelectContext(ctx, ®Rows, query, snapshotType, limit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make([]VcenterTotalRow, 0, len(regRows))
|
||||
for _, r := range regRows {
|
||||
if err := ValidateTableName(r.TableName); err != nil {
|
||||
continue
|
||||
}
|
||||
agg, err := aggregateSummaryTotals(ctx, dbConn, r.TableName, vcenter)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, VcenterTotalRow{
|
||||
SnapshotTime: r.SnapshotTime,
|
||||
Vcenter: vcenter,
|
||||
VmCount: agg.VmCount,
|
||||
VcpuTotal: agg.VcpuTotal,
|
||||
RamTotalGB: agg.RamTotalGB,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type summaryAgg struct {
|
||||
VmCount int64 `db:"vm_count"`
|
||||
VcpuTotal int64 `db:"vcpu_total"`
|
||||
RamTotalGB int64 `db:"ram_total"`
|
||||
}
|
||||
|
||||
// aggregateSummaryTotals computes totals for a single summary table (daily/monthly) for a given vcenter.
|
||||
func aggregateSummaryTotals(ctx context.Context, dbConn *sqlx.DB, tableName string, vcenter string) (summaryAgg, error) {
|
||||
if _, err := SafeTableName(tableName); err != nil {
|
||||
return summaryAgg{}, err
|
||||
}
|
||||
driver := strings.ToLower(dbConn.DriverName())
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
COUNT(1) AS vm_count,
|
||||
COALESCE(SUM(COALESCE("AvgVcpuCount","VcpuCount")),0) AS vcpu_total,
|
||||
COALESCE(SUM(COALESCE("AvgRamGB","RamGB")),0) AS ram_total
|
||||
FROM %s
|
||||
WHERE "Vcenter" = $1
|
||||
`, tableName)
|
||||
if driver == "sqlite" {
|
||||
query = strings.ReplaceAll(query, "$1", "?")
|
||||
}
|
||||
var agg summaryAgg
|
||||
if err := dbConn.GetContext(ctx, &agg, query, vcenter); err != nil {
|
||||
return summaryAgg{}, err
|
||||
}
|
||||
return agg, nil
|
||||
}
|
||||
|
||||
// SyncVcenterTotalsFromSnapshots backfills vcenter_totals using hourly snapshot tables in snapshot_registry.
|
||||
func SyncVcenterTotalsFromSnapshots(ctx context.Context, dbConn *sqlx.DB) error {
|
||||
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
||||
return err
|
||||
}
|
||||
driver := strings.ToLower(dbConn.DriverName())
|
||||
var hourlyTables []struct {
|
||||
TableName string `db:"table_name"`
|
||||
SnapshotTime int64 `db:"snapshot_time"`
|
||||
}
|
||||
if err := dbConn.SelectContext(ctx, &hourlyTables, `
|
||||
SELECT table_name, snapshot_time
|
||||
FROM snapshot_registry
|
||||
WHERE snapshot_type = 'hourly'
|
||||
ORDER BY snapshot_time
|
||||
`); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ht := range hourlyTables {
|
||||
if err := ValidateTableName(ht.TableName); err != nil {
|
||||
continue
|
||||
}
|
||||
// Aggregate per vcenter from the snapshot table.
|
||||
query := fmt.Sprintf(`
|
||||
SELECT "Vcenter" AS vcenter,
|
||||
COUNT(1) AS vm_count,
|
||||
COALESCE(SUM(CASE WHEN "VcpuCount" IS NOT NULL THEN "VcpuCount" ELSE 0 END), 0) AS vcpu_total,
|
||||
COALESCE(SUM(CASE WHEN "RamGB" IS NOT NULL THEN "RamGB" ELSE 0 END), 0) AS ram_total
|
||||
FROM %s
|
||||
GROUP BY "Vcenter"
|
||||
`, ht.TableName)
|
||||
type aggRow struct {
|
||||
Vcenter string `db:"vcenter"`
|
||||
VmCount int64 `db:"vm_count"`
|
||||
VcpuTotal int64 `db:"vcpu_total"`
|
||||
RamTotal int64 `db:"ram_total"`
|
||||
}
|
||||
var aggs []aggRow
|
||||
if err := dbConn.SelectContext(ctx, &aggs, query); err != nil {
|
||||
continue
|
||||
}
|
||||
for _, a := range aggs {
|
||||
// Insert if missing.
|
||||
insert := `
|
||||
INSERT INTO vcenter_totals ("Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB")
|
||||
SELECT $1,$2,$3,$4,$5
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM vcenter_totals WHERE "Vcenter" = $1 AND "SnapshotTime" = $2
|
||||
)
|
||||
`
|
||||
if driver == "sqlite" {
|
||||
insert = strings.ReplaceAll(insert, "$", "?")
|
||||
}
|
||||
_, _ = dbConn.ExecContext(ctx, insert, a.Vcenter, ht.SnapshotTime, a.VmCount, a.VcpuTotal, a.RamTotal)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AnalyzeTableIfPostgres runs ANALYZE on a table to refresh planner stats.
|
||||
func AnalyzeTableIfPostgres(ctx context.Context, dbConn *sqlx.DB, tableName string) {
|
||||
if _, err := SafeTableName(tableName); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user